diff --git a/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java b/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java index b8e6ee3..f9dbbb6 100644 --- a/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java +++ b/src/main/java/com/devcard/devcard/auth/config/SecurityConfig.java @@ -38,7 +38,8 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { "/notice", "/notice/**", "/qna", - "/qna/**" + "/qna/**", + "/shared/**" ).permitAll(); // 그 외의 모든 요청은 인증 필요 auth.anyRequest().authenticated(); diff --git a/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java b/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java index 25c5553..3bb95f6 100644 --- a/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java +++ b/src/main/java/com/devcard/devcard/card/controller/page/CardPageController.java @@ -78,4 +78,11 @@ public String editCardView(@PathVariable("cardId") Long cardId, Model model) { return "card-edit"; } + @GetMapping("/shared/cards/{cardId}") + public String sharedCardView(@PathVariable("cardId") Long cardId, Model model) { + CardResponseDto card = cardService.getCard(cardId); + model.addAttribute("card", card); + return "card-shared-view"; + } + } diff --git a/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java b/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java index a73e2a2..39b7466 100644 --- a/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java +++ b/src/main/java/com/devcard/devcard/card/controller/rest/QrController.java @@ -1,31 +1,35 @@ -package com.devcard.devcard.card.controller.rest; - -import com.devcard.devcard.card.dto.QrResponseDto; -import com.devcard.devcard.card.service.QrServiceImpl; -import com.google.zxing.WriterException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -import java.io.IOException; - -@RestController -public class QrController { - - private final QrServiceImpl qrServiceImpl; - - @Autowired - public QrController(QrServiceImpl qrServiceImpl) { - this.qrServiceImpl = qrServiceImpl; - } - - @GetMapping("/cards/{card_id}/qrcode") - public ResponseEntity createQR(@PathVariable (name = "card_id") Long cardId) throws IOException, WriterException { - String qrUrl = qrServiceImpl.createQr(cardId); - - return ResponseEntity.ok() - .body(new QrResponseDto(qrUrl)); - } -} +package com.devcard.devcard.card.controller.rest; + +import com.devcard.devcard.card.service.QrServiceImpl; +import com.google.zxing.WriterException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@RestController +public class QrController { + + private final QrServiceImpl qrServiceImpl; + + @Autowired + public QrController(QrServiceImpl qrServiceImpl) { + this.qrServiceImpl = qrServiceImpl; + } + + @GetMapping("/cards/{card_id}/qrcode-image") + public ResponseEntity generateQrImage(@PathVariable(name = "card_id") Long cardId) throws IOException, WriterException { + ByteArrayOutputStream qrImageStream = qrServiceImpl.generateQrImageStream(cardId); + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"qrcode.png\"") + .contentType(MediaType.IMAGE_PNG) + .body(qrImageStream.toByteArray()); + } +} diff --git a/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java b/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java deleted file mode 100644 index 7da8c17..0000000 --- a/src/main/java/com/devcard/devcard/card/dto/QrResponseDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.devcard.devcard.card.dto; - -public record QrResponseDto( - String qrcode_url -) { -} diff --git a/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java b/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java index f37e000..7e453ac 100644 --- a/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java +++ b/src/main/java/com/devcard/devcard/card/service/QrServiceImpl.java @@ -1,81 +1,36 @@ -package com.devcard.devcard.card.service; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -@Service -public class QrServiceImpl implements QrService{ - - private static final int QR_SIZE_WIDTH = 200; - private static final int QR_SIZE_HEIGHT = 200; - - @Value("${qr.domain.uri}") - private String domainUri; - - @Value("${qr.code.directory}") - private String qrCodeDirectory; - - /** - * @param cardId QR로 만들 명함 ID - * @return QR 코드 IMAGE 파일 이름만 반환 - */ - @Override - public String createQr(Long cardId) throws WriterException, IOException { - - // QR URL - QR 코드 정보 URL - String url = generateQrUrl(cardId); - - // QR Code - BitMatrix: qr code 정보 생성 - BitMatrix bitMatrix = generateQrCode(url); - - // Setting QR Image File Name, Path - String qrFileName = generateQrFileName(cardId); - Path qrPath = generateQrFilePath(qrFileName); - - // Save QR - saveQrCodeImage(bitMatrix, qrPath); - - return domainUri + "qrcodes/" + qrFileName; - } - - private String generateQrUrl(Long cardId) { - return domainUri + "cards/" + cardId + "/view"; - } - - private BitMatrix generateQrCode(String url) throws WriterException { - try { - return new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, QR_SIZE_WIDTH, QR_SIZE_HEIGHT); - } catch (WriterException e) { - throw e; - } - } - - private String generateQrFileName(Long cardId) { - return "card_id_" + cardId + ".png"; - } - - private Path generateQrFilePath(String qrFileName) { - return Paths.get(qrCodeDirectory + qrFileName); - } - - private void saveQrCodeImage(BitMatrix bitMatrix, Path qrPath) throws IOException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - MatrixToImageWriter.writeToStream(bitMatrix, "png", out); - Files.createDirectories(qrPath.getParent()); - Files.write(qrPath, out.toByteArray()); - } catch (IOException e) { - throw e; - } - } -} +package com.devcard.devcard.card.service; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +@Service +public class QrServiceImpl { + + private static final int QR_SIZE_WIDTH = 200; + private static final int QR_SIZE_HEIGHT = 200; + + @Value("${qr.domain.uri}") + private String domainUri; + + public ByteArrayOutputStream generateQrImageStream(Long cardId) throws WriterException, IOException { + // QR URL 생성 + String url = domainUri + "shared/cards/" + cardId; + + // QR Code 생성 + BitMatrix bitMatrix = new MultiFormatWriter().encode(url, BarcodeFormat.QR_CODE, QR_SIZE_WIDTH, QR_SIZE_HEIGHT); + + // QR Code 이미지를 메모리 스트림에 저장 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream); + + return outputStream; + } +} diff --git a/src/main/resources/static/css/card/card-detail.css b/src/main/resources/static/css/card/card-detail.css index b7fc4e2..4338987 100644 --- a/src/main/resources/static/css/card/card-detail.css +++ b/src/main/resources/static/css/card/card-detail.css @@ -187,4 +187,12 @@ .delete-button.button:hover { background-color: #16a085; /* Darker red */ -} \ No newline at end of file +} + +#qr-image-container { + display: none; + text-align: center; + margin-top: 10px; /* 기존 20px에서 10px로 줄임 */ + position: relative; /* 컨테이너의 위치를 세부 조정할 수 있도록 설정 */ + top: -10px; /* 컨테이너를 위로 이동 */ +} diff --git a/src/main/resources/static/js/card/card-share.js b/src/main/resources/static/js/card/card-share.js index c32d0d0..8487b32 100644 --- a/src/main/resources/static/js/card/card-share.js +++ b/src/main/resources/static/js/card/card-share.js @@ -1,3 +1,4 @@ +// Kakao 공유 버튼 document.getElementById('kakao-share-btn').addEventListener('click', function () { if (!cardId) { console.error('Card ID가 제공되지 않았습니다.'); @@ -5,17 +6,16 @@ document.getElementById('kakao-share-btn').addEventListener('click', function () return; } - // 기본 content 설정 + // Kakao 공유 설정 const content = { title: `${cardName}님의 명함`, imageUrl: 'https://developers.kakao.com/assets/img/about/logos/kakaolink/kakaolink_btn_medium.png', link: { - mobileWebUrl: `http://3.34.144.148:8080/cards/${cardId}/view`, - webUrl: `http://3.34.144.148:8080/cards/${cardId}/view` + mobileWebUrl: `http://3.34.144.148:8080/shared/cards/${cardId}`, + webUrl: `http://3.34.144.148:8080/shared/cards/${cardId}` } }; - // description이 있는 경우에만 추가 if (cardCompany || cardPosition) { content.description = `회사: ${cardCompany || ''}${cardCompany && cardPosition ? ', ' : ''}직책: ${cardPosition || ''}`; } @@ -27,20 +27,21 @@ document.getElementById('kakao-share-btn').addEventListener('click', function () { title: '명함 보기', link: { - mobileWebUrl: `http://3.34.144.148:8080/cards/${cardId}/view`, - webUrl: `http://3.34.144.148:8080/cards/${cardId}/view` + mobileWebUrl: `http://3.34.144.148:8080/shared/cards/${cardId}`, + webUrl: `http://3.34.144.148:8080/shared/cards/${cardId}` } } ], - fail: function(error) { + fail: function (error) { console.error(error); handleError('카카오톡 공유에 실패했습니다.', 300); } }); }); +// QR 생성 및 렌더링 document.getElementById('qr-share-btn').addEventListener('click', function () { - const cardId = this.getAttribute('data-card-id'); // QR 버튼에서 cardId 가져오기 + const cardId = this.getAttribute('data-card-id'); // 버튼에서 cardId 가져오기 if (!cardId) { console.error('Card ID가 제공되지 않았습니다.'); @@ -48,32 +49,27 @@ document.getElementById('qr-share-btn').addEventListener('click', function () { return; } - fetch(`/cards/${cardId}/qrcode`) + const qrContainer = document.getElementById('qr-image-container'); + qrContainer.innerHTML = ''; // 이전 QR 코드 제거 + + // QR 코드 API 요청 + fetch(`/cards/${cardId}/qrcode-image?t=${new Date().getTime()}`) .then(response => { - if (!response.ok) { - console.error(`서버 응답 오류: ${response.status} ${response.statusText}`); - return response.text(); - } - return response.json(); + if (!response.ok) throw new Error('QR 코드 생성에 실패했습니다.'); + return response.blob(); }) - .then(data => { - console.log(data) - const qrUrl = `${data.qrcode_url}?t=${new Date().getTime()}`; // 타임스탬프 추가 + .then(blob => { const qrImage = document.createElement('img'); - qrImage.src = qrUrl; + qrImage.src = URL.createObjectURL(blob); qrImage.alt = 'QR Code'; qrImage.style.width = '200px'; - qrImage.style.height = 'auto'; - - const qrContainer = document.getElementById('qr-image-container'); - qrContainer.innerHTML = ''; // 기존 내용 지우기 - qrContainer.appendChild(qrImage); // QR 코드 이미지 추가 + qrImage.style.height = '200px'; - qrContainer.style.display = 'block'; - handleSuccess('QR 코드가 성공적으로 생성되었습니다.', 400); + qrContainer.appendChild(qrImage); // QR 이미지 추가 + qrContainer.style.display = 'block'; // QR 컨테이너 표시 }) .catch(error => { - console.error('QR 코드 fetch 오류:', error); + console.error('QR 코드 생성 중 오류:', error); handleError('QR 코드 생성에 실패했습니다.', 400); }); }); diff --git a/src/main/resources/templates/card-detail.html b/src/main/resources/templates/card-detail.html index 6e6fb92..4f784e6 100644 --- a/src/main/resources/templates/card-detail.html +++ b/src/main/resources/templates/card-detail.html @@ -100,6 +100,8 @@

그룹 선택

const cardCompany = /*[[${card.company}]]*/ ''; // 회사 이름 const cardPosition = /*[[${card.position}]]*/ ''; // 직책 + + diff --git a/src/main/resources/templates/card-shared-view.html b/src/main/resources/templates/card-shared-view.html new file mode 100644 index 0000000..e962136 --- /dev/null +++ b/src/main/resources/templates/card-shared-view.html @@ -0,0 +1,114 @@ + + + + + + [[${card.name}]] 명함 + + + + + + + + + + + + + + + +
+

[[${card.name}]] 명함

+ +
+
+ +
+
+ + + + +
+ + +
+ + + + +