Skip to content

Commit

Permalink
✨ Feature/#12 - 식권 QR 코드 조회하기 API 구현 (#51)
Browse files Browse the repository at this point in the history
* ✨ feature/#12 : 5.4 식권 QR 코드 조회하기 response dto 정의

Signed-off-by: EunJiJung <[email protected]>

* ✨ feature/#12: QR 코드 생성 util 구현

Signed-off-by: EunJiJung <[email protected]>

* ♻️ refactor/#12 : QrUtil exception 처리

Signed-off-by: EunJiJung <[email protected]>

* ♻️ refactor/#12 : qr response dto string -> byte[]로 변경

Signed-off-by: EunJiJung <[email protected]>

* ✨ feature/#12: QR 코드 생성 서비스 구현

Signed-off-by: EunJiJung <[email protected]>

* ♻️ refactor/#12 : qr 사이즈 153 x 153px로 변경

Signed-off-by: EunJiJung <[email protected]>

* ✨ feature/#12 : qr code color, 로고 이미지 추가

Signed-off-by: EunJiJung <[email protected]>

* ✨ feature/#12 : 5.4 식권 QR코드 조회하기 http 메서드 코드 추가

Signed-off-by: EunJiJung <[email protected]>

* ♻️ refactor/#12 : QrUtil ticketId -> id로 네이밍 변경

Signed-off-by: EunJiJung <[email protected]>

* ♻️ refactor/#12 : QrUtil tCommonException 정의

Signed-off-by: EunJiJung <[email protected]>

* ✨ feature/#12 : aes 암호화 util 추가, qr id 암호화 추가

Signed-off-by: EunJiJung <[email protected]>

---------

Signed-off-by: EunJiJung <[email protected]>
  • Loading branch information
bianbbc87 authored Nov 19, 2024
1 parent b0430cd commit 6c4d0ee
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 5 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ dependencies {
// Testing Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// QR code - zxing
implementation group: 'com.google.zxing', name: 'javase', version: '3.5.0'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.0'
}

tasks.named('test') {
Expand Down
5 changes: 5 additions & 0 deletions http/Event/EventControllerHttpRequest.http
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@
GET {{host_url}}/api/v1/tickets?
page={{event.API_5_3.page}}&
size={{event.API_5_3.size}}
Authorization: Bearer {{access_token}}

### 5.4 식권 QR코드 조회하기
// @no-log
GET {{host_url}}/api/v1/tickets/{{event.API_5_4.id}}/briefs
Authorization: Bearer {{access_token}}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ public enum ErrorCode {

// External Server Error
EXTERNAL_SERVER_ERROR(50200, HttpStatus.BAD_GATEWAY, "서버 외부 에러입니다."),
;

// Qr Code Error
QR_CODE_GENERATION_ERROR(50300, HttpStatus.INTERNAL_SERVER_ERROR, "QR 코드 생성 중 오류가 발생했습니다."),
QR_CODE_IMAGE_PROCESSING_ERROR(50301, HttpStatus.INTERNAL_SERVER_ERROR, "QR 코드 이미지 처리 중 오류가 발생했습니다."),

// AES Error
AES_ENCRYPTION_ERROR(50400, HttpStatus.INTERNAL_SERVER_ERROR, "AES 암호화 중 오류가 발생했습니다."),
AES_DECRYPTION_ERROR(50401, HttpStatus.INTERNAL_SERVER_ERROR, "AES 복호화 중 오류가 발생했습니다.");


private final Integer code;
Expand Down
82 changes: 82 additions & 0 deletions src/main/java/com/daon/onjung/core/utility/AESUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.daon.onjung.core.utility;
import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

@Component
public class AESUtil {

@Value("${aes-util.algorithm}")
private String algorithm;

@Value("${aes-util.secret-key}")
private String secret_key;

private static final SecureRandom random = new SecureRandom();

// 랜덤 16바이트 IV 생성
public byte[] generateRandomIV() {
byte[] iv = new byte[16]; // 16바이트 IV
random.nextBytes(iv);
return iv;
}

// 공통된 암호화/복호화 처리
private Cipher getCipher(int mode, byte[] keyBytes, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(mode, secretKeySpec, ivParameterSpec);
return cipher;
}

// AES 암호화
public String encrypt_AES(String id) {
try {
byte[] keyBytes = Base64.getDecoder().decode(secret_key);
byte[] iv = generateRandomIV();

Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, keyBytes, iv);
byte[] encrypted = cipher.doFinal(id.getBytes());

// IV와 암호화된 데이터를 결합 (IV + Ciphertext)
byte[] combined = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

return Base64.getEncoder().encodeToString(combined);

} catch (Exception e) {
throw new CommonException(ErrorCode.AES_ENCRYPTION_ERROR);
}
}

// AES 복호화
public String decrypt_AES(String encryptedText) {
try {
byte[] combined = Base64.getDecoder().decode(encryptedText);
byte[] keyBytes = Base64.getDecoder().decode(secret_key);

// IV와 암호화된 데이터 분리
byte[] iv = new byte[16];
byte[] encrypted = new byte[combined.length - 16];
System.arraycopy(combined, 0, iv, 0, 16);
System.arraycopy(combined, 16, encrypted, 0, encrypted.length);

Cipher cipher = getCipher(Cipher.DECRYPT_MODE, keyBytes, iv);
byte[] decrypted = cipher.doFinal(encrypted);

return new String(decrypted);

} catch (Exception e) {
throw new CommonException(ErrorCode.AES_DECRYPTION_ERROR);
}
}
}
120 changes: 120 additions & 0 deletions src/main/java/com/daon/onjung/core/utility/QrUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.daon.onjung.core.utility;

import com.daon.onjung.core.exception.error.ErrorCode;
import com.daon.onjung.core.exception.type.CommonException;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class QrUtil {

// QR을 확인해야 하는 웹 서버 주소
@Value("${qr-web.url}")
private String url;
@Value("${qr-web.path}")
private String path;

@Value("${qr-web.logo_img_url}")
private String logo_img_url;
@Value("${qr-web.bg_img_url}")
private String bg_img_url;

@Value("${qr-web.qr-color-code}")
private String qr_color_code;
@Value("${qr-web.bg-color-code}")
private String bg_color_code;

/**
* 주어진 링크를 인코딩하여 QR 코드 이미지를 생성하고,
* 배경 이미지와 로고를 결합하여 byte 배열 형태로 반환하는 메서드
*
* @param id
* @return 배경 이미지와 합성된 QR 코드 이미지를 바이트 배열 형태로 반환
*/
public byte[] generateQrCodeImageByte(String id) {
try {

// QR 코드 데이터 생성
String baseUrl = url + path + id;

// QR 코드 생성 옵션 설정
Map<EncodeHintType, Object> hintMap = new HashMap<>();
hintMap.put(EncodeHintType.MARGIN, 0); // 여백 없음
hintMap.put(EncodeHintType.CHARACTER_SET, "UTF-8");

// QR 코드 생성
QRCodeWriter qrCodeWriter = new QRCodeWriter();
int qrSize = 153; // QR 코드와 배경 이미지 크기를 동일하게 설정
BitMatrix bitMatrix = qrCodeWriter.encode(baseUrl, BarcodeFormat.QR_CODE, qrSize, qrSize, hintMap);

// 색상 지정
MatrixToImageConfig matrixToImageConfig = new MatrixToImageConfig(Long.decode(qr_color_code).intValue(),Long.decode(bg_color_code).intValue());

BufferedImage qrCodeImage = MatrixToImageWriter.toBufferedImage(bitMatrix, matrixToImageConfig);

// 배경 이미지 로드 (디자인 파일 경로) - 배경이 없으면 로고 이미지가 흰색으로 안 보이는 이슈 있음.
BufferedImage originalBackgroundImage = ImageIO.read(new File(bg_img_url));

// 배경 이미지를 QR 코드 크기로 리사이즈
BufferedImage backgroundImage = new BufferedImage(qrSize, qrSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D bgGraphics = backgroundImage.createGraphics();
bgGraphics.drawImage(originalBackgroundImage, 0, 0, qrSize, qrSize, null);
bgGraphics.dispose();

// 로고 이미지 로드
BufferedImage logoImage = ImageIO.read(new File(logo_img_url));

// 배경 이미지 위에 QR 코드 합성
BufferedImage combinedImage = new BufferedImage(qrSize, qrSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = combinedImage.createGraphics();

// 배경 이미지 그리기
g.drawImage(backgroundImage, 0, 0, null);

// QR 코드 중앙에 그리기
int qrX = (qrSize - qrCodeImage.getWidth()) / 2;
int qrY = (qrSize - qrCodeImage.getHeight()) / 2;
g.drawImage(qrCodeImage, qrX, qrY, null);

// QR 코드 중앙에 로고 삽입
int logoWidth = qrCodeImage.getWidth() / 4; // QR 코드 크기의 1/4 크기로 로고 설정
int logoHeight = qrCodeImage.getHeight() / 4;
int logoX = qrX + (qrCodeImage.getWidth() - logoWidth) / 2; // QR 코드 중앙에 배치
int logoY = qrY + (qrCodeImage.getHeight() - logoHeight) / 2;
g.drawImage(logoImage, logoX, logoY, logoWidth, logoHeight, null);

// 리소스 해제
g.dispose();

// 결과 이미지를 바이트 배열로 변환
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(combinedImage, "png", byteArrayOutputStream);
byteArrayOutputStream.flush();
byte[] combinedImageBytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();

return combinedImageBytes;

} catch (WriterException e) {
throw new CommonException(ErrorCode.QR_CODE_GENERATION_ERROR);
} catch (IOException e) {
throw new CommonException(ErrorCode.QR_CODE_IMAGE_PROCESSING_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

import com.daon.onjung.core.annotation.security.AccountID;
import com.daon.onjung.core.dto.ResponseDto;
import com.daon.onjung.event.application.dto.response.ReadTicketBriefResponseDto;
import com.daon.onjung.event.application.dto.response.ReadTicketResponseDto;
import com.daon.onjung.event.application.usecase.ReadTicketBriefUseCase;
import com.daon.onjung.event.application.usecase.ReadTicketUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

Expand All @@ -18,6 +17,7 @@
public class TicketQueryV1Controller {

private final ReadTicketUseCase readTicketUseCase;
private final ReadTicketBriefUseCase readTicketBriefUseCase;

/**
* 5.3 나의 식권 조회하기
Expand All @@ -36,4 +36,18 @@ public ResponseDto<ReadTicketResponseDto> readTicketList(
)
);
}

/**
* 5.4 식권 QR코드 조회하기
*/
@GetMapping("/tickets/{id}/briefs")
public ResponseDto<ReadTicketBriefResponseDto> readTicketBrief(
@PathVariable Long id
) {
return ResponseDto.ok(
readTicketBriefUseCase.execute(
id
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.daon.onjung.event.application.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;

@Getter
public class ReadTicketBriefResponseDto {

@NotNull(message = "qr_base64는 null일 수 없습니다.")
@JsonProperty("qr_base64")
private final byte[] qrBase64;

@Builder
public ReadTicketBriefResponseDto(
byte[] qrBase64
) {
this.qrBase64 = qrBase64;
}

public static ReadTicketBriefResponseDto fromEntity(
byte[] qrBase64
) {

return ReadTicketBriefResponseDto.builder()
.qrBase64(qrBase64)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.daon.onjung.event.application.service;

import com.daon.onjung.core.utility.AESUtil;
import com.daon.onjung.core.utility.QrUtil;
import com.daon.onjung.event.application.dto.response.ReadTicketBriefResponseDto;
import com.daon.onjung.event.application.usecase.ReadTicketBriefUseCase;
import com.daon.onjung.event.repository.mysql.TicketRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ReadTicketBriefService implements ReadTicketBriefUseCase {

private final TicketRepository ticketRepository;

private final QrUtil qrUtil;
private final AESUtil aesUtil;

@Override
@Transactional(readOnly = true)
public ReadTicketBriefResponseDto execute(Long id) {

// 암호화
String encryptedId = aesUtil.encrypt_AES(id.toString());

byte[] qrCodeBytes = qrUtil.generateQrCodeImageByte(encryptedId);

return ReadTicketBriefResponseDto.fromEntity(qrCodeBytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.daon.onjung.event.application.usecase;

import com.daon.onjung.core.annotation.bean.UseCase;
import com.daon.onjung.event.application.dto.response.ReadTicketBriefResponseDto;

@UseCase
public interface ReadTicketBriefUseCase {

/**
* 식권 QR 코드 조회하기
* @param id 티켓 ID
* @return 식권 QR 코드 조회 응답 DTO
*/
ReadTicketBriefResponseDto execute(Long id);
}

0 comments on commit 6c4d0ee

Please sign in to comment.