diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 230993b..900e2b6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -## Chacnged +## Changed > 간단한 설명 (예: 로그인 완료 후 세션 동기화 누락 문제 해결) 수정 내용 - 예) 로그인 성공 시 `store.setSession()` 호출 추가 diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index efe64c5..6cc0d4a 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -28,7 +28,7 @@ jobs: else echo "HOST=${{ secrets.TEST_HOST }}" >> $GITHUB_ENV echo "TARGET_DIR=${{ secrets.TEST_TARGET_DIR }}" >> $GITHUB_ENV - echo "${{ secrets.APP_PROPS_MAIN }}" > ./src/main/resources/application.properties + echo "${{ secrets.APP_PROPS_MAIN_BASE64 }}" | base64 --decode > ./src/main/resources/application.properties fi - name: Build jar file @@ -44,7 +44,8 @@ jobs: ./Dockerfile \ ${{ secrets.USERNAME }}@${{ env.HOST }}:${{ env.TARGET_DIR }} - - name: SSH into EC2 and run Docker + - name: SSH into EC2 and run Docker (main + if: github.ref == 'refs/heads/main' run: | ssh -o ServerAliveInterval=30 -i key.pem -o StrictHostKeyChecking=no ${{ secrets.USERNAME }}@${{ env.HOST }} << EOF cd $TARGET_DIR @@ -52,4 +53,30 @@ jobs: sudo docker rm backend || true sudo docker build -t backend . sudo docker run -d -p 8080:8080 --name backend backend - EOF \ No newline at end of file + EOF + - name: SSH into server and deploy (release - blue/green) + if: github.ref == 'refs/heads/release' + run: | + ssh -o ServerAliveInterval=30 -i key.pem -o StrictHostKeyChecking=no \ + ${{ secrets.USERNAME }}@${{ env.HOST }} << 'EOF' + + cd $TARGET_DIR + + # 현재 active 확인 (nginx 기준) + if grep -q "8081" /etc/nginx/sites-available/barogagi.xyz; then + TARGET="blue" + else + TARGET="green" + fi + + echo "Deploy target: $TARGET" + + # 이미지 빌드 + docker build -t barogagi-backend:single . + + # inactive 컨테이너만 재기동 + docker compose up -d backend-$TARGET + + # 트래픽 전환 + /srv/barogagi/backend/scripts/switch.sh + EOF diff --git a/Dockerfile b/Dockerfile index d88c088..7c4252a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:17 +FROM eclipse-temurin:17-jdk WORKDIR /app COPY *.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/src/main/java/com/SwaggerConfig.java b/src/main/java/com/SwaggerConfig.java index 85beadd..95c7828 100644 --- a/src/main/java/com/SwaggerConfig.java +++ b/src/main/java/com/SwaggerConfig.java @@ -1,13 +1,27 @@ package com; import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.springframework.context.annotation.Configuration; @OpenAPIDefinition( info = @Info(title = "[PROJECT] barogagi API", version = "v1", description = "프로젝트 바로가기 API 명세서") ) - +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" +) +@SecurityScheme( + name = "apiKeyAuth", + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.HEADER, + paramName = "API-KEY" +) @Configuration public class SwaggerConfig { } diff --git a/src/main/java/com/barogagi/ai/client/AIClient.java b/src/main/java/com/barogagi/ai/client/AIClient.java new file mode 100644 index 0000000..0ab4b0b --- /dev/null +++ b/src/main/java/com/barogagi/ai/client/AIClient.java @@ -0,0 +1,113 @@ +package com.barogagi.ai.client; + +import com.barogagi.ai.dto.AIReqWrapper; +import com.barogagi.ai.dto.AIResDTO; +import com.barogagi.ai.dto.ChatMessage; +import com.barogagi.ai.dto.ChatRequest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.barogagi.util.HtmlUtils.stripCodeFence; + +@Service +public class AIClient { + private static final Logger logger = LoggerFactory.getLogger(AIClient.class); + + @Value("${ai.api.base-url}") + private String aiBaseUrl; + + @Value("${ai.api.path}") + private String aiPath; + + @Value("${ai.api.key}") + private String aiApiKey; + + @Value("${ai.prompt.system}") + private String aiPrompt; + + @Value("${ai.model}") + private String aiModel; + + private final RestTemplate restTemplate = new RestTemplate(); + private static final ObjectMapper OM = new ObjectMapper(); + + public AIResDTO recommandPlace(AIReqWrapper aiReqWrapper) { + String url = aiBaseUrl + aiPath; + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(aiApiKey); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(List.of(MediaType.APPLICATION_JSON)); + + ChatRequest chatRequest = buildChatRequest(aiReqWrapper); + HttpEntity entity = new HttpEntity<>(chatRequest, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class + ); + + String body = response.getBody(); + + ObjectMapper om = new ObjectMapper(); + try { + // 1) content 추출 + JsonNode root = om.readTree(body); + JsonNode contentNode = root.path("choices").get(0).path("message").path("content"); + String content = contentNode.asText(); + + // 2) 코드펜스/여분 공백 제거 + content = stripCodeFence(content); + + // 3) content(JSON 문자열) -> DTO + AIResDTO dto = om.readValue(content, AIResDTO.class); + return dto; + + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + // JsonMappingException 포함해서 모두 여기서 처리됨 + return null; // 혹은 throw new IllegalStateException("AI 응답 파싱 실패", e); + } + } + + // ai에게 요청 가능한 형태로 변경 + private ChatRequest buildChatRequest(AIReqWrapper wrapper) { + // system 메시지 + ChatMessage systemMsg = ChatMessage.builder() + .role("system") + .content(aiPrompt) + .build(); + + // user 메시지: AIReqWrapper → 문자열 변환 + StringBuilder sb = new StringBuilder(); + sb.append("comment: ").append(wrapper.getComment()).append("\n"); + sb.append("tags: ").append(String.join(", ", wrapper.getTags())).append("\n\n"); + + sb.append("places:\n"); + String placesText = wrapper.getPlaceList().stream() + .map(p -> "- title: " + p.getTitle() + "\n description: " + p.getDescription()) + .collect(Collectors.joining("\n")); + sb.append(placesText); + + ChatMessage userMsg = ChatMessage.builder() + .role("user") + .content(sb.toString()) + .build(); + + return ChatRequest.builder() + .model(aiModel) + .messages(List.of(systemMsg, userMsg)) + .max_tokens(500) + .build(); + } +} diff --git a/src/main/java/com/barogagi/ai/dto/AIReqDTO.java b/src/main/java/com/barogagi/ai/dto/AIReqDTO.java new file mode 100644 index 0000000..95f8fbc --- /dev/null +++ b/src/main/java/com/barogagi/ai/dto/AIReqDTO.java @@ -0,0 +1,13 @@ +package com.barogagi.ai.dto; + +import lombok.*; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +public class AIReqDTO { + private String title; + private String description; +} diff --git a/src/main/java/com/barogagi/ai/dto/AIReqWrapper.java b/src/main/java/com/barogagi/ai/dto/AIReqWrapper.java new file mode 100644 index 0000000..53fc097 --- /dev/null +++ b/src/main/java/com/barogagi/ai/dto/AIReqWrapper.java @@ -0,0 +1,15 @@ +package com.barogagi.ai.dto; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +public class AIReqWrapper { + private List tags; + private String comment; + private List placeList; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/ai/dto/AIResDTO.java b/src/main/java/com/barogagi/ai/dto/AIResDTO.java new file mode 100644 index 0000000..506fc57 --- /dev/null +++ b/src/main/java/com/barogagi/ai/dto/AIResDTO.java @@ -0,0 +1,13 @@ +package com.barogagi.ai.dto; + +import lombok.*; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +public class AIResDTO { + private Integer recommandPlaceIndex; + private String aiDescription; +} diff --git a/src/main/java/com/barogagi/ai/dto/ChatMessage.java b/src/main/java/com/barogagi/ai/dto/ChatMessage.java new file mode 100644 index 0000000..2042ec6 --- /dev/null +++ b/src/main/java/com/barogagi/ai/dto/ChatMessage.java @@ -0,0 +1,13 @@ +package com.barogagi.ai.dto; + +import lombok.*; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +public class ChatMessage { + private String role; // "system" | "user" | "assistant" + private String content; // 메시지 내용 +} diff --git a/src/main/java/com/barogagi/ai/dto/ChatRequest.java b/src/main/java/com/barogagi/ai/dto/ChatRequest.java new file mode 100644 index 0000000..61f9355 --- /dev/null +++ b/src/main/java/com/barogagi/ai/dto/ChatRequest.java @@ -0,0 +1,16 @@ +package com.barogagi.ai.dto; + +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +public class ChatRequest { + private String model; + private List messages; + private int max_tokens; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/approval/controller/ApprovalController.java b/src/main/java/com/barogagi/approval/controller/ApprovalController.java index 8ab1cba..21b67cb 100644 --- a/src/main/java/com/barogagi/approval/controller/ApprovalController.java +++ b/src/main/java/com/barogagi/approval/controller/ApprovalController.java @@ -1,192 +1,53 @@ package com.barogagi.approval.controller; import com.barogagi.approval.service.ApprovalService; -import com.barogagi.approval.service.AuthCodeService; import com.barogagi.approval.vo.ApprovalCompleteVO; import com.barogagi.approval.vo.ApprovalSendVO; -import com.barogagi.approval.vo.ApprovalVO; import com.barogagi.response.ApiResponse; -import com.barogagi.sendSms.dto.SendSmsVO; -import com.barogagi.sendSms.service.SendSmsService; -import com.barogagi.util.EncryptUtil; -import com.barogagi.util.InputValidate; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.*; @Tag(name = "인증", description = "인증 API") @RestController -@RequestMapping("/approval") +@RequestMapping("/api/v1/verification-codes") public class ApprovalController { - private static final Logger logger = LoggerFactory.getLogger(ApprovalController.class); - @Autowired - private InputValidate inputValidate; - - @Autowired - private EncryptUtil encryptUtil; - - @Autowired - private AuthCodeService authCodeService; - - @Autowired - private ApprovalService approvalService; - - @Autowired - private SendSmsService sendSmsService; - - private final String API_SECRET_KEY; + private final ApprovalService approvalService; @Autowired - public ApprovalController(Environment environment){ - this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + public ApprovalController(ApprovalService approvalService){ + this.approvalService = approvalService; } - @Operation(summary = "인증번호 발송", description = "휴대전화번호로 인증번호 발송하는 기능입니다.") - @PostMapping("/authCode/send") + @Operation(summary = "인증번호 발송", description = "휴대전화번호로 인증번호 발송하는 기능입니다.
회원가입 시 사용할 경우 type 값을 JOIN-MEMBERSHIP 값으로 넣어주세요.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A200", description = "인증번호 발송에 성공하었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A102", description = "인증문자 발송 중 오류가 발생하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A103", description = "인증번호 발송에 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/send") public ApiResponse approvalTelSend(@RequestBody ApprovalSendVO approvalSendVO) { - - logger.info("CALL /approval/tel/authCode/send!"); - logger.info("[input] API_SECRET_KEY={}", approvalSendVO.getApiSecretKey()); - - ApprovalVO approvalVO = new ApprovalVO(); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - if(approvalSendVO.getApiSecretKey().equals(API_SECRET_KEY)) { - - if(inputValidate.isEmpty(approvalSendVO.getTel())){ - resultCode = "101"; - message = "인증번호를 발송할 전화번호를 입력해주세요."; - - } else{ - - // 전화번호 - String recipientTel = approvalSendVO.getTel(); - - // 인증번호를 DB에 INSERT 전에, 전에 발송된 기록들은 flag UPDATE 처리 - approvalVO.setCompleteYn("N"); - approvalVO.setType(approvalSendVO.getType()); - - // 전화번호 암호화 - approvalVO.setTel(encryptUtil.hashEncodeString(approvalSendVO.getTel())); - - int updateResult = approvalService.updateApprovalRecord(approvalVO); - logger.info("@@ updateResult={}", updateResult); - - // 인증번호 생성 - String authCode = authCodeService.generateAuthCode(); - logger.info("@@ authCode={}", authCode); - - // 인증번호 메시지 발송 - SendSmsVO sendSmsVO = new SendSmsVO(); - sendSmsVO.setRecipientTel(recipientTel); - String messageContent = "인증번호는 [" + authCode + "] 입니다."; - sendSmsVO.setMessageContent(messageContent); - boolean sendMessageResult = sendSmsService.sendSms(sendSmsVO); - logger.info("@@ sendMessageResult={}", sendMessageResult); - - // 인증번호 암호화 - approvalVO.setAuthCode(encryptUtil.hashEncodeString(authCode)); - - // 인증번호를 DB에 insert - if(sendMessageResult){ - approvalVO.setMessageContent(sendSmsVO.getMessageContent()); - int insertResult = approvalService.insertApprovalRecord(approvalVO); - logger.info("@@ insertResult={}", insertResult); - if(insertResult > 0) { - // 인증번호 발송 로직 - resultCode = "200"; - message = "인증번호 발송에 성공하었습니다."; - - } else{ - resultCode = "102"; - message = "오류가 발생하였습니다."; - } - } else { - resultCode = "103"; - message = "인증번호 발송에 실패하였습니다."; - } - } - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + return approvalService.approvalTelSend(approvalSendVO); } - @Operation(summary = "인증번호 일치 여부 확인", description = "휴대전화번호에 발송된 인증번호와 입력된 인증번호가 동일한지 확인") - @PostMapping("/authCode/check") + @Operation(summary = "인증번호 일치 여부 확인", description = "휴대전화번호에 발송된 인증번호와 입력된 인증번호가 동일한지 확인." + + "
회원가입 시 사용할 경우 type 값을 JOIN-MEMBERSHIP 값으로 넣어주세요." + + "
authCode에는 인증번호를 넣어주세요." + + "
tel에는 전화번호를 넣어주세요.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A200", description = "인증이 완료되었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A300", description = "인증이 실패하었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/verify") public ApiResponse approvalTelCheck(@RequestBody ApprovalCompleteVO approvalCompleteVO) { - - logger.info("CALL /approval/tel/authCode/check"); - logger.info("[input] API_SECRET_KEY={}", approvalCompleteVO.getApiSecretKey()); - - ApprovalVO approvalVO = new ApprovalVO(); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if(approvalCompleteVO.getApiSecretKey().equals(API_SECRET_KEY)) { - if(inputValidate.isEmpty(approvalCompleteVO.getAuthCode())){ - resultCode = "101"; - message = "인증번호를 입력해주세요."; - - } else{ - logger.info("@@@@ authCode = {}", approvalCompleteVO.getAuthCode()); - - // 전화번호 암호화 - approvalVO.setTel(encryptUtil.hashEncodeString(approvalCompleteVO.getTel())); - approvalVO.setCompleteYn("N"); - approvalVO.setAuthCode(encryptUtil.hashEncodeString(approvalCompleteVO.getAuthCode())); - approvalVO.setType(approvalCompleteVO.getType()); - - logger.info("authcode = {}", encryptUtil.hashEncodeString(approvalCompleteVO.getAuthCode())); - - int updateResult = approvalService.updateApprovalComplete(approvalVO); - logger.info("@@ updateResult={}", updateResult); - - if(updateResult == 1){ - resultCode = "200"; - message = "인증이 완료되었습니다."; - } else { - resultCode = "300"; - message = "인증에 실패하였습니다."; - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + return approvalService.approvalTelCheck(approvalCompleteVO); } } diff --git a/src/main/java/com/barogagi/approval/exception/ApprovalException.java b/src/main/java/com/barogagi/approval/exception/ApprovalException.java new file mode 100644 index 0000000..62b8ca6 --- /dev/null +++ b/src/main/java/com/barogagi/approval/exception/ApprovalException.java @@ -0,0 +1,13 @@ +package com.barogagi.approval.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class ApprovalException extends BusinessException { + + public ApprovalException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/barogagi/approval/service/ApprovalService.java b/src/main/java/com/barogagi/approval/service/ApprovalService.java index 661a2ec..2dcca08 100644 --- a/src/main/java/com/barogagi/approval/service/ApprovalService.java +++ b/src/main/java/com/barogagi/approval/service/ApprovalService.java @@ -1,7 +1,17 @@ package com.barogagi.approval.service; +import com.barogagi.approval.exception.ApprovalException; import com.barogagi.approval.mapper.ApprovalMapper; +import com.barogagi.approval.vo.ApprovalCompleteVO; +import com.barogagi.approval.vo.ApprovalSendVO; import com.barogagi.approval.vo.ApprovalVO; +import com.barogagi.response.ApiResponse; +import com.barogagi.sendSms.dto.SendSmsVO; +import com.barogagi.sendSms.service.SendSmsService; +import com.barogagi.util.EncryptUtil; +import com.barogagi.util.InputValidate; +import com.barogagi.util.Validator; +import com.barogagi.util.exception.ErrorCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -9,10 +19,110 @@ public class ApprovalService { private final ApprovalMapper approvalMapper; + private final Validator validator; + private final InputValidate inputValidate; + private final EncryptUtil encryptUtil; + private final AuthCodeService authCodeService; + private final SendSmsService sendSmsService; @Autowired - public ApprovalService(ApprovalMapper approvalMapper){ + public ApprovalService( + ApprovalMapper approvalMapper, + Validator validator, + InputValidate inputValidate, + EncryptUtil encryptUtil, + AuthCodeService authCodeService, + SendSmsService sendSmsService + ) + { this.approvalMapper = approvalMapper; + this.validator = validator; + this.inputValidate = inputValidate; + this.encryptUtil = encryptUtil; + this.authCodeService = authCodeService; + this.sendSmsService = sendSmsService; + } + + public ApiResponse approvalTelSend(ApprovalSendVO approvalSendVO) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(approvalSendVO.getApiSecretKey())) { + throw new ApprovalException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(approvalSendVO.getTel())) { + throw new ApprovalException(ErrorCode.EMPTY_DATA); + } + + // 3. 처리 + // 인증번호를 DB에 INSERT 전에, 전에 발송된 기록들은 flag UPDATE 처리 + ApprovalVO approvalVO = new ApprovalVO(); + approvalVO.setCompleteYn("N"); + approvalVO.setType(approvalSendVO.getType()); + + approvalSendVO.setTel(approvalSendVO.getTel().replaceAll("[^0-9]", "")); + + // 전화번호 암호화 + approvalVO.setTel(encryptUtil.hashEncodeString(approvalSendVO.getTel())); + + int updateResult = this.updateApprovalRecord(approvalVO); + + // 인증번호 생성 + String authCode = authCodeService.generateAuthCode(); + + // 인증번호 메시지 발송 + SendSmsVO sendSmsVO = new SendSmsVO(); + sendSmsVO.setRecipientTel(approvalSendVO.getTel()); + String messageContent = "인증번호는 [" + authCode + "] 입니다."; + sendSmsVO.setMessageContent(messageContent); + boolean sendMessageResult = sendSmsService.sendSms(sendSmsVO); + + if(!sendMessageResult) { + throw new ApprovalException(ErrorCode.FAIL_SEND_SMS); + } + + // 인증번호 암호화 + approvalVO.setAuthCode(encryptUtil.hashEncodeString(authCode)); + + approvalVO.setMessageContent(sendSmsVO.getMessageContent()); + int insertResult = this.insertApprovalRecord(approvalVO); + + if(insertResult <= 0) { + throw new ApprovalException(ErrorCode.ERROR_SEND_SMS); + } + + return ApiResponse.result(ErrorCode.SUCCESS_SEND_SMS); + } + + public ApiResponse approvalTelCheck(ApprovalCompleteVO approvalCompleteVO) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(approvalCompleteVO.getApiSecretKey())) { + throw new ApprovalException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(approvalCompleteVO.getAuthCode()) + || inputValidate.isEmpty(approvalCompleteVO.getTel())) { + throw new ApprovalException(ErrorCode.EMPTY_DATA); + } + + // 3. 전화번호 암호화 + ApprovalVO approvalVO = new ApprovalVO(); + approvalCompleteVO.setTel(approvalCompleteVO.getTel().replaceAll("[^0-9]", "")); + approvalVO.setTel(encryptUtil.hashEncodeString(approvalCompleteVO.getTel())); + approvalVO.setCompleteYn("N"); + approvalVO.setAuthCode(encryptUtil.hashEncodeString(approvalCompleteVO.getAuthCode())); + approvalVO.setType(approvalCompleteVO.getType()); + + // 4. 인증 + int updateResult = this.updateApprovalComplete(approvalVO); + if(updateResult != 1){ + throw new ApprovalException(ErrorCode.FAIL_CHECK_SMS); + } + + return ApiResponse.result(ErrorCode.SUCCESS_CHECK_SMS); } public int updateApprovalRecord(ApprovalVO vo){ diff --git a/src/main/java/com/barogagi/config/JwtAuthFilter.java b/src/main/java/com/barogagi/config/JwtAuthFilter.java index 8d9e99f..233b7df 100644 --- a/src/main/java/com/barogagi/config/JwtAuthFilter.java +++ b/src/main/java/com/barogagi/config/JwtAuthFilter.java @@ -2,8 +2,9 @@ import com.barogagi.member.info.dto.Member; import com.barogagi.member.info.service.MemberService; -import com.barogagi.member.login.repository.UserMembershipRepository; +import com.barogagi.member.login.exception.InvalidRefreshTokenException; import com.barogagi.util.JwtUtil; +import com.barogagi.util.exception.ErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.*; @@ -13,18 +14,17 @@ import org.springframework.security.oauth2.jwt.JwtException; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; + import java.io.IOException; @Component public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwt; - private final UserMembershipRepository userRepo; private final MemberService memberService; - public JwtAuthFilter(JwtUtil jwt, UserMembershipRepository userRepo, MemberService memberService) { + public JwtAuthFilter(JwtUtil jwt, MemberService memberService) { this.jwt = jwt; - this.userRepo = userRepo; this.memberService = memberService; } @@ -36,11 +36,9 @@ protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, String header = req.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); - Claims claims = jwt.parseToken(token, "ACCESS"); String membershipNo = jwt.getMembershipNo(claims); - // 회원 조회 Member member = memberService.findByMembershipNo(membershipNo); @@ -55,12 +53,12 @@ protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, chain.doFilter(req, res); } catch (ExpiredJwtException e) { // 유효기간이 지나서 만료된 경우 - writeErrorResponse(res, "TOKEN_EXPIRED", "Access token has expired"); + writeErrorResponse(ErrorCode.EXPIRE_TOKEN); } catch (JwtException | SecurityException e) { // 위조되었거나 변조되었거나 구조가 잘못되었을 경우 - writeErrorResponse(res, "REVOKED_TOKEN", "Revoked access token"); + writeErrorResponse(ErrorCode.NOT_EXIST_ACCESS_AUTH); } catch (Exception e) { - writeErrorResponse(res, "UNKNOWN_ERROR", "Unknown authentication error"); + writeErrorResponse(ErrorCode.NOT_EXIST_ACCESS_AUTH); } } @@ -68,19 +66,11 @@ protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, @Override protected boolean shouldNotFilter(HttpServletRequest request) { String p = request.getRequestURI(); - return p.startsWith("/auth/"); + return p.startsWith("/auth/") || p.startsWith("/login/basic/membership/userId/search"); } - private void writeErrorResponse(HttpServletResponse res, String errorCode, String message) throws IOException { - res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - res.setContentType("application/json;charset=UTF-8"); - - String json = String.format( - "{\"errorCode\":\"%s\", \"message\":\"%s\"}", - errorCode, message - ); - - res.getWriter().write(json); + private void writeErrorResponse(ErrorCode errorCode) throws IOException { + throw new InvalidRefreshTokenException(errorCode); } } diff --git a/src/main/java/com/barogagi/config/OAuth2LoginSuccessHandler.java b/src/main/java/com/barogagi/config/OAuth2LoginSuccessHandler.java index 12a90f3..f2a30c2 100644 --- a/src/main/java/com/barogagi/config/OAuth2LoginSuccessHandler.java +++ b/src/main/java/com/barogagi/config/OAuth2LoginSuccessHandler.java @@ -42,6 +42,8 @@ public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res.setContentType("application/json;charset=UTF-8"); objectMapper.writeValue(res.getWriter(), Map.of( + "resultCode", login.tokens().resultCode(), + "message", login.tokens().message(), "accessToken", login.tokens().accessToken(), "accessTokenExpiresIn", login.tokens().accessTokenExpiresIn(), "userId", userId, diff --git a/src/main/java/com/barogagi/config/SecurityConfig.java b/src/main/java/com/barogagi/config/SecurityConfig.java index 1fb8774..0be7381 100644 --- a/src/main/java/com/barogagi/config/SecurityConfig.java +++ b/src/main/java/com/barogagi/config/SecurityConfig.java @@ -2,7 +2,10 @@ import com.barogagi.member.oauth.join.service.CustomOidcUserService; import com.barogagi.member.oauth.join.service.DelegatingOAuth2UserService; +import com.barogagi.util.exception.ErrorCode; import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -16,6 +19,8 @@ @EnableWebSecurity public class SecurityConfig { + Logger logger = LoggerFactory.getLogger(SecurityConfig.class); + private final JwtAuthFilter jwtAuthFilter; private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; @@ -23,6 +28,7 @@ public SecurityConfig(JwtAuthFilter jwtAuthFilter, OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler) { this.jwtAuthFilter = jwtAuthFilter; this.oAuth2LoginSuccessHandler = oAuth2LoginSuccessHandler; + logger.info("@@ jwtAuthFilter={}", jwtAuthFilter); } private static final String[] PERMIT_URL_ARRAY = { @@ -33,10 +39,12 @@ public SecurityConfig(JwtAuthFilter jwtAuthFilter, "/webjars/**", "/login/oauth2/**", "/oauth2/**", - "/auth/**", - "/login/**", - "/membership/join/**", - "/terms/**" + "/api/v1/auth/**", // 일반 회원가입 관련 + "/api/v1/users/**", // 로그인 관련 + "/api/v1/terms/**", // 약관 관련 + "/api/v1/home/tags/popular", // 인기 태그 조회 + "/api/v1/home/regions/popular", // 인기 지역 조회 + "/api/v1/verification-codes/**" }; @Bean @@ -68,9 +76,17 @@ SecurityFilterChain filterChain( .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) // 브라우저 리다이렉트 대신 401 JSON .exceptionHandling(ex -> ex.authenticationEntryPoint((req, res, e) -> { + + String resultCode = ErrorCode.NOT_EXIST_ACCESS_AUTH.getCode(); + String message = ErrorCode.NOT_EXIST_ACCESS_AUTH.getMessage(); + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); res.setContentType("application/json;charset=UTF-8"); - res.getWriter().write("{\"error\":\"unauthorized\"}"); + String json = String.format( + "{\"resultCode\":\"%s\", \"message\":\"%s\"}", + resultCode, message + ); + res.getWriter().write(json); })); return http.build(); diff --git a/src/main/java/com/barogagi/config/exception/BusinessException.java b/src/main/java/com/barogagi/config/exception/BusinessException.java new file mode 100644 index 0000000..0ef97df --- /dev/null +++ b/src/main/java/com/barogagi/config/exception/BusinessException.java @@ -0,0 +1,20 @@ +package com.barogagi.config.exception; + +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public abstract class BusinessException extends RuntimeException { + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.httpStatus = errorCode.getStatus(); + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + } +} diff --git a/src/main/java/com/barogagi/config/resultCode/ProcessResultCode.java b/src/main/java/com/barogagi/config/resultCode/ProcessResultCode.java new file mode 100644 index 0000000..a2d9f18 --- /dev/null +++ b/src/main/java/com/barogagi/config/resultCode/ProcessResultCode.java @@ -0,0 +1,82 @@ +package com.barogagi.config.resultCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ProcessResultCode { + + EMPTY_DATA("101", "정보를 입력해주세요."), + + // nickname + INVALID_NICKNAME("102", "적합하지 않는 닉네임입니다."), + UNAVAILABLE_NICKNAME("103", "해당 닉네임 사용이 불가능합니다."), + AVAILABLE_NICKNAME("200", "사용 가능한 닉네임입니다."), + + // userId + INVALID_USER_ID("102", "적합한 아이디가 아닙니다."), + UNAVAILABLE_USER_ID("300", "해당 아이디 사용이 불가능합니다."), + AVAILABLE_USER_ID("200", "해당 아이디 사용이 가능합니다."), + + // signUp + INVALID_SIGN_UP("102", "적합한 아이디, 비밀번호, 닉네임이 아닙니다."), + SUCCESS_SIGN_UP("200", "회원가입에 성공하였습니다."), + FAIL_SIGN_UP("300", "회원가입에 실패하였습니다."), + + // deleteAccount + SUCCESS_DELETE_ACCOUNT("200", "회원 탈퇴되었습니다."), + FAIL_DELETE_ACCOUNT("300", "회원 탈퇴 실패하였습니다."), + + // findUser + FOUND_ACCOUNT("200", "해당 전화번호로 가입된 아이디가 존재합니다."), + NOT_FOUND_ACCOUNT("201", "해당 전화번호로 가입된 계정이 존재하지 않습니다."), + + // updatePassword + SUCCESS_UPDATE_PASSWORD("200", "비밀번호 재설정에 성공하였습니다."), + FAIL_UPDATE_PASSWORD("300", "비밀번호 재설정에 실패하였습니다."), + + // Login + NOT_FOUND_USER_INFO("102", "회원 정보가 존재하지 않습니다."), + FAIL_LOGIN("103", "로그인에 실패하였습니다."), + SUCCESS_LOGIN("200", "로그인에 성공하였습니다."), + + // refreshToken + REQUIRED_LOGIN("110", "로그인을 진행해주세요."), + REQUIRED_RE_LOGIN("120", "로그인을 다시 진행해주세요."), + SUCCESS_REFRESH_TOKEN("200", "토큰이 발급되었습니다."), + FAIL_REFRESH_TOKEN("130", "토큰 발급에 실패하였습니다."), + + // logout + FAIL_LOGOUT("300", "로그아웃 실패하였습니다."), + SUCCESS_LOGOUT("200", "로그아웃 되었습니다."), + + // terms + FOUND_TERMS("200", "약관 조회에 성공하였습니다."), + NOT_FOUND_TERMS("102", "약관이 존재하지 않습니다."), + SUCCESS_INSERT_TERMS("200", "약관 저장에 성공하였습니다."), + FAIL_INSERT_TERMS("300", "약관 저장에 실패하였습니다."), + + // memberInfo + FOUND_USER_INFO("200", "회원 정보 조회가 완료되었습니다."), + FAIL_UPDATE_USER_INFO("404", "사용자 정보 수정 실패하였습니다."), + SUCCESS_UPDATE_USER_INFO("200", "사용자 정보 수정 완료하였습니다."), + + // mainPage + NOT_FOUND_SCHEDULE("201", "일정이 존재하지 않습니다."), + FOUND_SCHEDULE("200", "조회 성공하였습니다."), + NOT_FOUND_POPULAR_TAG("201", "인기 태그 목록이 존재하지 않습니다."), + FOUND_POPULAR_TAG("200", "인기 태그 조회 완료하였습니다."), + NOT_FOUND_POPULAR_REGION("201", "인기 지역 목록이 존재하지 않습니다."), + FOUND_POPULAR_REGION("200", "인기 지역 조회 완료하였습니다."), + + // approval + SUCCESS_SEND_SMS("200", "인증번호 발송에 성공하었습니다."), + FAIL_SEND_SMS("103", "인증번호 발송에 실패하였습니다."), + ERROR_SEND_SMS("102", "오류가 발생하였습니다."), + SUCCESS_CHECK_SMS("200", "인증이 완료되었습니다."), + FAIL_CHECK_SMS("300", "인증에 실패하였습니다."); + + private final String resultCode; + private final String message; +} diff --git a/src/main/java/com/barogagi/config/resultCode/ResultCode.java b/src/main/java/com/barogagi/config/resultCode/ResultCode.java new file mode 100644 index 0000000..a854ce5 --- /dev/null +++ b/src/main/java/com/barogagi/config/resultCode/ResultCode.java @@ -0,0 +1,23 @@ +package com.barogagi.config.resultCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ResultCode { + + // API_SECRET_KEY 일치 X + NOT_EQUAL_API_SECRET_KEY("100", "잘못된 접근입니다."), + + // ACCESS TOKEN + NOT_EXIST_ACCESS_AUTH("401", "접근 권한이 존재하지 않습니다."), + EXIST_ACCESS_AUTH("200", "회원 번호가 존재합니다."), + EXPIRE_TOKEN("300", "Token이 만료되었습니다."), + + // 서버 오류 + ERROR("400","오류가 발생하였습니다."); + + private final String resultCode; + private final String message; +} diff --git a/src/main/java/com/barogagi/kakaoplace/client/KakaoGeoCodeClient.java b/src/main/java/com/barogagi/kakaoplace/client/KakaoGeoCodeClient.java new file mode 100644 index 0000000..611e604 --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/client/KakaoGeoCodeClient.java @@ -0,0 +1,49 @@ +package com.barogagi.kakaoplace.client; + +import com.barogagi.kakaoplace.dto.KakaoGeoCodeResDTO; +import com.barogagi.kakaoplace.dto.KakaoGeoCodeSearchResDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; + +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; + +@Service +public class KakaoGeoCodeClient { + private static final Logger logger = LoggerFactory.getLogger(KakaoGeoCodeClient.class); + + @Value("${kakao.rest-api-key}") + private String kakaoApiKey; + + private final RestTemplate restTemplate = new RestTemplate(); + + public List convertKakaoGeoCode(String address) { + String url = String.valueOf(UriComponentsBuilder +// .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/address.json") + .queryParam("query", address) + .build(false)); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + KakaoGeoCodeSearchResDTO.class + ); + + List body = response.getBody().getDocuments(); + return body; + } +} diff --git a/src/main/java/com/barogagi/kakaoplace/client/KakaoPlaceClient.java b/src/main/java/com/barogagi/kakaoplace/client/KakaoPlaceClient.java new file mode 100644 index 0000000..559c38c --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/client/KakaoPlaceClient.java @@ -0,0 +1,76 @@ +package com.barogagi.kakaoplace.client; + +import com.barogagi.kakaoplace.dto.KakaoPlaceResDTO; +import com.barogagi.kakaoplace.dto.KakaoPlaceSearchResDTO; +import com.barogagi.schedule.command.service.ScheduleCommandService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; + +@Service +public class KakaoPlaceClient { + private static final Logger logger = LoggerFactory.getLogger(KakaoPlaceClient.class); + + @Value("${kakao.rest-api-key}") + private String kakaoApiKey; + + private final RestTemplate restTemplate = new RestTemplate(); + + public List searchKakaoPlace(String query, String x, String y, int radius, int limitPlace) { + String url = String.valueOf(UriComponentsBuilder + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") + .queryParam("query", query) + .queryParam("x", x) + .queryParam("y", y) + .queryParam("radius", radius) + .queryParam("size", limitPlace) + .build(false)); + + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + KakaoPlaceSearchResDTO.class + ); + + List body = response.getBody().getDocuments(); + return body; + } + + public List searchKakaoPlaceByKeyword(String query) { + String url = String.valueOf(UriComponentsBuilder + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") + .queryParam("query", query) + .build(false)); + + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + KakaoPlaceSearchResDTO.class + ); + + List body = response.getBody().getDocuments(); + return body; + } + +} diff --git a/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeResDTO.java b/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeResDTO.java new file mode 100644 index 0000000..d05a996 --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeResDTO.java @@ -0,0 +1,19 @@ +package com.barogagi.kakaoplace.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "카카오 주소로 좌표 변환 DTO") +public class KakaoGeoCodeResDTO { + @JsonProperty("x") + private String x; + + @JsonProperty("y") + private String y; +} diff --git a/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeSearchResDTO.java b/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeSearchResDTO.java new file mode 100644 index 0000000..33b7245 --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/dto/KakaoGeoCodeSearchResDTO.java @@ -0,0 +1,18 @@ +package com.barogagi.kakaoplace.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "카카오 주소로 좌표 변환 결과 DTO") +public class KakaoGeoCodeSearchResDTO { + @JsonProperty("documents") + private List documents; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceResDTO.java b/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceResDTO.java new file mode 100644 index 0000000..815c7d0 --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceResDTO.java @@ -0,0 +1,46 @@ +package com.barogagi.kakaoplace.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Getter @Setter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "카카오 장소 추천 목록 DTO") +public class KakaoPlaceResDTO { + + @JsonProperty("address_name") + private String addressName; + + @JsonProperty("place_name") + private String placeName; + + @JsonProperty("category_group_name") + private String categoryGroupName; + + @JsonProperty("distance") + private String distance; + + @JsonProperty("id") + private String id; + + @JsonProperty("x") + private String x; + + @JsonProperty("y") + private String y; + + @JsonProperty("place_url") + private String placeUrl; + + @JsonProperty("road_address_name") + private String roadAddressName; + + @JsonProperty("phone") + private String phone; + + private Integer regionNum; +} diff --git a/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceSearchResDTO.java b/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceSearchResDTO.java new file mode 100644 index 0000000..2aa7f50 --- /dev/null +++ b/src/main/java/com/barogagi/kakaoplace/dto/KakaoPlaceSearchResDTO.java @@ -0,0 +1,19 @@ +package com.barogagi.kakaoplace.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "카카오 장소 추천 목록 결과 DTO") +public class KakaoPlaceSearchResDTO { + @JsonProperty("documents") + private List documents; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/logging/ControllerLoggingAspect.java b/src/main/java/com/barogagi/logging/ControllerLoggingAspect.java new file mode 100644 index 0000000..240551b --- /dev/null +++ b/src/main/java/com/barogagi/logging/ControllerLoggingAspect.java @@ -0,0 +1,27 @@ +package com.barogagi.logging; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +public class ControllerLoggingAspect { + + @Around("@within(org.springframework.web.bind.annotation.RestController)") + public Object logController(ProceedingJoinPoint joinPoint) throws Throwable { + + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + log.info("Controller Start - {}.{}", className, methodName); + + Object result = joinPoint.proceed(); + log.info("Controller End - {}.{}", className, methodName); + + return result; + } +} diff --git a/src/main/java/com/barogagi/logging/ServiceLoggingAspect.java b/src/main/java/com/barogagi/logging/ServiceLoggingAspect.java new file mode 100644 index 0000000..d32a1da --- /dev/null +++ b/src/main/java/com/barogagi/logging/ServiceLoggingAspect.java @@ -0,0 +1,44 @@ +package com.barogagi.logging; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.response.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +public class ServiceLoggingAspect { + + @Around("execution(* com.barogagi..service..*(..))") + public Object logService(ProceedingJoinPoint joinPoint) throws Throwable { + + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + long startTime = System.currentTimeMillis(); + + log.info("Service Start - {}.{}", className, methodName); + + try { + Object result = joinPoint.proceed(); + + long elapsedTime = System.currentTimeMillis() - startTime; + log.info("Service End - {}.{}, time={}ms", + className, methodName, elapsedTime); + + return result; + } catch (BusinessException ex) { + log.error("Service BusinessException - {}.{} / httpStatus - {} / errorCode - {} / message - {}", className, methodName, ex.getHttpStatus(), ex.getCode(), ex.getMessage(), ex); + throw ex; + + } catch (Exception e) { + log.error("Service Exception - {}.{}", className, methodName, e); + throw e; + } + } +} + diff --git a/src/main/java/com/barogagi/mainPage/controller/MainPageController.java b/src/main/java/com/barogagi/mainPage/controller/MainPageController.java new file mode 100644 index 0000000..71f2d21 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/controller/MainPageController.java @@ -0,0 +1,59 @@ +package com.barogagi.mainPage.controller; + +import com.barogagi.mainPage.response.MainPageResponse; +import com.barogagi.mainPage.service.MainPageService; +import com.barogagi.response.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "메인 화면", description = "메인 화면에 필요한 API") +@RestController +@RequestMapping("/api/v1/home") +public class MainPageController { + + private final MainPageService mainPageService; + + @Autowired + public MainPageController(MainPageService mainPageService) { + this.mainPageService = mainPageService; + } + + @Operation(summary = "유저 일정 정보 API", description = "메인 화면 - 다가오는 일정 부분에 해당하는 API", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A401", description = "접근 권한이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M201", description = "일정이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M200", description = "조회 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping("/me/schedules") + public MainPageResponse selectUserScheduleInfo(HttpServletRequest request) { + return mainPageService.selectUserScheduleInfoProcess(request); + } + + @Operation(summary = "인기 태그 조회 API ", description = "메인 화면 - 오늘 많이 생성되는 일정 부분에 해당하는 API", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M200", description = "인기 태그 조회 완료하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M201", description = "인기 태그 목록이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping("/tags/popular") + public ApiResponse selectPopularTagList(@RequestHeader("API-KEY") String apiSecretKey) { + return mainPageService.selectPopularTagList(apiSecretKey); + } + + @Operation(summary = "인기 지역 조회 API ", description = "메인 화면 - 지금 인기많은 핫 플레이스 부분에 해당하는 API", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M200", description = "인기 지역 조회 완료하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M201", description = "인기 지역 목록이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping("/regions/popular") + public ApiResponse selectPopularRegionList(@RequestHeader("API-KEY") String apiSecretKey) { + return mainPageService.selectPopularRegionList(apiSecretKey); + } +} diff --git a/src/main/java/com/barogagi/mainPage/dto/RegionInfoDTO.java b/src/main/java/com/barogagi/mainPage/dto/RegionInfoDTO.java new file mode 100644 index 0000000..1fd195c --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/RegionInfoDTO.java @@ -0,0 +1,13 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegionInfoDTO { + private String regionLevel1 = ""; + private String regionLevel2 = ""; + private String regionLevel3 = ""; + private String regionLevel4 = ""; +} diff --git a/src/main/java/com/barogagi/mainPage/dto/RegionRankInfoDTO.java b/src/main/java/com/barogagi/mainPage/dto/RegionRankInfoDTO.java new file mode 100644 index 0000000..998479f --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/RegionRankInfoDTO.java @@ -0,0 +1,14 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegionRankInfoDTO { + private String regionLevel1 = ""; + private String regionLevel2 = ""; + private String regionLevel3 = ""; + private String regionLevel4 = ""; + private int rankNo = 0; +} diff --git a/src/main/java/com/barogagi/mainPage/dto/TagInfoDTO.java b/src/main/java/com/barogagi/mainPage/dto/TagInfoDTO.java new file mode 100644 index 0000000..3f220f0 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/TagInfoDTO.java @@ -0,0 +1,13 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TagInfoDTO { + + private int tagNum = 0; + private String tagNm; + private String tagType; +} diff --git a/src/main/java/com/barogagi/mainPage/dto/TagRankInfoDTO.java b/src/main/java/com/barogagi/mainPage/dto/TagRankInfoDTO.java new file mode 100644 index 0000000..8faf910 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/TagRankInfoDTO.java @@ -0,0 +1,12 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TagRankInfoDTO { + + private String tagNm = ""; + private int rankNo = 0; +} diff --git a/src/main/java/com/barogagi/mainPage/dto/UserInfoRequestDTO.java b/src/main/java/com/barogagi/mainPage/dto/UserInfoRequestDTO.java new file mode 100644 index 0000000..a50c69b --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/UserInfoRequestDTO.java @@ -0,0 +1,17 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserInfoRequestDTO { + + private String membershipNo = ""; + + // SCHEDULE + private int scheduleNum = 0; + + // PLAN + private int planNum = 0; +} diff --git a/src/main/java/com/barogagi/mainPage/dto/UserInfoResponseDTO.java b/src/main/java/com/barogagi/mainPage/dto/UserInfoResponseDTO.java new file mode 100644 index 0000000..880df60 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/dto/UserInfoResponseDTO.java @@ -0,0 +1,20 @@ +package com.barogagi.mainPage.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserInfoResponseDTO { + + // SCHEDULE + private int scheduleNum = 0; + private String scheduleNm = ""; + private String startDate = ""; + + // PLAN + private int planNum = 0; + + // CATEGORY + private String categoryNm = ""; +} diff --git a/src/main/java/com/barogagi/mainPage/exception/MainPageException.java b/src/main/java/com/barogagi/mainPage/exception/MainPageException.java new file mode 100644 index 0000000..c08b15e --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/exception/MainPageException.java @@ -0,0 +1,12 @@ +package com.barogagi.mainPage.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class MainPageException extends BusinessException { + public MainPageException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/barogagi/mainPage/mapper/MainPageMapper.java b/src/main/java/com/barogagi/mainPage/mapper/MainPageMapper.java new file mode 100644 index 0000000..e2163e0 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/mapper/MainPageMapper.java @@ -0,0 +1,25 @@ +package com.barogagi.mainPage.mapper; + +import com.barogagi.mainPage.dto.*; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MainPageMapper { + + // 유저 일정 조회 + UserInfoResponseDTO selectUserScheduleInfo(UserInfoRequestDTO userInfoRequestDTO); + + // 해당 schedule에 대한 태그 목록 조회 + List selectScheduleTag(UserInfoRequestDTO userInfoRequestDTO); + + // 해당 plan에 대한 region 정보 조회 + RegionInfoDTO selectScheduleRegionInfo(UserInfoRequestDTO userInfoRequestDTO); + + // 인기 지역 조회 + List selectRegionRankList(); + + // 인기 태그 조회 + List selectTagRankList(); +} diff --git a/src/main/java/com/barogagi/mainPage/response/MainPageResponse.java b/src/main/java/com/barogagi/mainPage/response/MainPageResponse.java new file mode 100644 index 0000000..972c4d4 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/response/MainPageResponse.java @@ -0,0 +1,44 @@ +package com.barogagi.mainPage.response; + +import com.barogagi.mainPage.dto.RegionInfoDTO; +import com.barogagi.mainPage.dto.TagInfoDTO; +import com.barogagi.mainPage.dto.UserInfoResponseDTO; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class MainPageResponse { + // 결과 코드 + private String resultCode; + + // 결과 메시지 + private String message; + + // 데이터 + private UserInfoResponseDTO userInfoResponseDTO; + private List tagInfoList; + private RegionInfoDTO regionInfoDTO; + + public static MainPageResponse resultData( + UserInfoResponseDTO userInfoResponseDTO, + List tagInfoList, + RegionInfoDTO regionInfoDTO, + String resultCode, + String message + ) + { + MainPageResponse mainPageResponse = new MainPageResponse(); + + mainPageResponse.userInfoResponseDTO = userInfoResponseDTO; + mainPageResponse.tagInfoList = tagInfoList; + mainPageResponse.regionInfoDTO = regionInfoDTO; + + mainPageResponse.resultCode = resultCode; + mainPageResponse.message = message; + + return mainPageResponse; + } +} diff --git a/src/main/java/com/barogagi/mainPage/service/MainPageService.java b/src/main/java/com/barogagi/mainPage/service/MainPageService.java new file mode 100644 index 0000000..4feaa12 --- /dev/null +++ b/src/main/java/com/barogagi/mainPage/service/MainPageService.java @@ -0,0 +1,153 @@ +package com.barogagi.mainPage.service; + +import com.barogagi.mainPage.dto.*; +import com.barogagi.mainPage.exception.MainPageException; +import com.barogagi.mainPage.mapper.MainPageMapper; +import com.barogagi.mainPage.response.MainPageResponse; +import com.barogagi.response.ApiResponse; +import com.barogagi.util.MembershipUtil; +import com.barogagi.util.Validator; +import com.barogagi.util.exception.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +@Service +public class MainPageService { + + private static final Logger logger = LoggerFactory.getLogger(MainPageService.class); + + private final MainPageMapper mainPageMapper; + private final MembershipUtil membershipUtil; + private final Validator validator; + + @Autowired + public MainPageService( + MainPageMapper mainPageMapper, + MembershipUtil membershipUtil, + Validator validator + ) + { + this.mainPageMapper = mainPageMapper; + this.membershipUtil = membershipUtil; + this.validator = validator; + } + + public MainPageResponse selectUserScheduleInfoProcess(HttpServletRequest request) { + + String resultCode = ""; + String message = ""; + List tagList = null; + RegionInfoDTO regionInfo = null; + UserInfoResponseDTO userInfoResponseDTO = null; + + // 1. 회원번호 구하기 + Map membershipNoInfo = membershipUtil.membershipNoService(request); + if(!membershipNoInfo.get("resultCode").equals("A200")) { + + return MainPageResponse.resultData( + userInfoResponseDTO, tagList, regionInfo, + String.valueOf(membershipNoInfo.get("resultCode")), + String.valueOf(membershipNoInfo.get("message")) + ); + } + + String membershipNo = String.valueOf(membershipNoInfo.get("membershipNo")); + + // 2. 유저 일정 정보 조회 + UserInfoRequestDTO userInfoRequestDTO = new UserInfoRequestDTO(); + userInfoRequestDTO.setMembershipNo(membershipNo); + userInfoResponseDTO = this.selectUserScheduleInfo(userInfoRequestDTO); + + if(null == userInfoResponseDTO) { + resultCode = ErrorCode.NOT_FOUND_SCHEDULE.getCode(); + message = ErrorCode.NOT_FOUND_SCHEDULE.getMessage(); + } else { + + resultCode = ErrorCode.FOUND_SCHEDULE.getCode(); + message = ErrorCode.FOUND_SCHEDULE.getMessage(); + + // 3. 해당 schedule에 대한 태그 목록 조회 + userInfoRequestDTO.setScheduleNum(userInfoResponseDTO.getScheduleNum()); + tagList = this.selectScheduleTag(userInfoRequestDTO); + + // 4. 해당 plan에 대한 region 정보 조회 + userInfoRequestDTO.setPlanNum(userInfoResponseDTO.getPlanNum()); + regionInfo = this.selectScheduleRegionInfo(userInfoRequestDTO); + } + + return MainPageResponse.resultData(userInfoResponseDTO, tagList, regionInfo, resultCode, message); + } + + public ApiResponse selectPopularTagList(String apiSecretKey) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(apiSecretKey)) { + throw new MainPageException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 인기 태그 조회 + List tagRankInfoList = this.selectTagRankList(); + if(tagRankInfoList.isEmpty()) { + throw new MainPageException(ErrorCode.NOT_FOUND_POPULAR_TAG); + + } + + return ApiResponse.resultData( + tagRankInfoList, + ErrorCode.FOUND_POPULAR_TAG.getCode(), + ErrorCode.FOUND_POPULAR_TAG.getMessage() + ); + } + + public ApiResponse selectPopularRegionList(String apiSecretKey) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(apiSecretKey)) { + throw new MainPageException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 인기 지역 조회 + List regionRankInfoList = this.selectRegionRankList(); + + if(regionRankInfoList.isEmpty()) { + throw new MainPageException(ErrorCode.NOT_FOUND_POPULAR_REGION); + } + + return ApiResponse.resultData( + regionRankInfoList, + ErrorCode.FOUND_POPULAR_REGION.getCode(), + ErrorCode.FOUND_POPULAR_REGION.getMessage() + ); + } + + // 유저 일정 조회 + public UserInfoResponseDTO selectUserScheduleInfo(UserInfoRequestDTO userInfoRequestDTO) { + return mainPageMapper.selectUserScheduleInfo(userInfoRequestDTO); + } + + // 해당 schedule에 대한 태그 목록 조회 + public List selectScheduleTag(UserInfoRequestDTO userInfoRequestDTO) { + return mainPageMapper.selectScheduleTag(userInfoRequestDTO); + } + + // 해당 plan에 대한 region 정보 조회 + public RegionInfoDTO selectScheduleRegionInfo(UserInfoRequestDTO userInfoRequestDTO) { + return mainPageMapper.selectScheduleRegionInfo(userInfoRequestDTO); + } + + // 인기 지역 조회 + public List selectRegionRankList() { + return mainPageMapper.selectRegionRankList(); + } + + // 인기 태그 조회 + public List selectTagRankList() { + return mainPageMapper.selectTagRankList(); + } +} diff --git a/src/main/java/com/barogagi/member/basic/join/controller/JoinController.java b/src/main/java/com/barogagi/member/basic/join/controller/JoinController.java index b29adc9..c749627 100644 --- a/src/main/java/com/barogagi/member/basic/join/controller/JoinController.java +++ b/src/main/java/com/barogagi/member/basic/join/controller/JoinController.java @@ -1,247 +1,82 @@ package com.barogagi.member.basic.join.controller; -import com.barogagi.config.PasswordConfig; -import com.barogagi.member.basic.join.dto.NickNameDTO; -import com.barogagi.member.basic.join.service.JoinService; -import com.barogagi.member.basic.join.dto.JoinDTO; -import com.barogagi.member.basic.join.dto.UserIdCheckDTO; +import com.barogagi.member.basic.join.service.BasicJoinService; import com.barogagi.member.basic.join.dto.JoinRequestDTO; +import com.barogagi.member.login.dto.RefreshTokenRequestDTO; import com.barogagi.response.ApiResponse; -import com.barogagi.util.EncryptUtil; -import com.barogagi.util.InputValidate; -import com.barogagi.util.Validator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.*; @Tag(name = "일반 회원가입", description = "일반 회원가입 관련 API") @RestController -@RequestMapping("/membership/join") +@RequestMapping("/api/v1/users") public class JoinController { - private static final Logger logger = LoggerFactory.getLogger(JoinController.class); - private final JoinService joinService; - private final InputValidate inputValidate; - private final EncryptUtil encryptUtil; - private final Validator validator; - private final PasswordConfig passwordConfig; - - private final String API_SECRET_KEY; + private final BasicJoinService basicJoinService; @Autowired - public JoinController(Environment environment, - JoinService joinService, - InputValidate inputValidate, - EncryptUtil encryptUtil, - Validator validator, - PasswordConfig passwordConfig) { - this.API_SECRET_KEY = environment.getProperty("api.secret-key"); - this.joinService = joinService; - this.inputValidate = inputValidate; - this.encryptUtil = encryptUtil; - this.validator = validator; - this.passwordConfig = passwordConfig; + public JoinController(BasicJoinService basicJoinService) { + this.basicJoinService = basicJoinService; } - @Operation(summary = "아이디 중복 체크 기능", description = "아이디 중복 체크 기능입니다.") - @PostMapping("/basic/membership/userId/check") - public ApiResponse checkUserId(@RequestBody UserIdCheckDTO userIdCheckDTO) { - - logger.info("CALL /membership/join/basic/membership/userId/check"); - logger.info("[input] API_SECRET_KEY={}", userIdCheckDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - if(userIdCheckDTO.getApiSecretKey().equals(API_SECRET_KEY)){ - - if(inputValidate.isEmpty(userIdCheckDTO.getUserId())) { - resultCode = "101"; - message = "아이디를 입력해주세요."; - } else{ - - if(!validator.isValidId(userIdCheckDTO.getUserId())) { - resultCode = "102"; - message = "적합한 아이디가 아닙니다."; - } else { - JoinDTO joinDTO = new JoinDTO(); - joinDTO.setUserId(userIdCheckDTO.getUserId()); - - int checkUserId = joinService.checkUserId(joinDTO); - logger.info("@@ checkUserId={}", checkUserId); - - if(checkUserId > 0){ - resultCode = "300"; - message = "해당 아이디 사용이 불가능합니다."; - - } else{ - resultCode = "200"; - message = "해당 아이디 사용이 가능합니다."; - } - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + @Operation(summary = "아이디 중복 체크 기능", description = "아이디 중복 체크 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U200", description = "해당 아이디 사용이 가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U102", description = "적합한 아이디가 아닙니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U300", description = "해당 아이디 사용이 불가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping("/userid/exists") + public ApiResponse checkUserId(@RequestHeader("API-KEY") String apiSecretKey, @RequestParam String userId) { + return basicJoinService.checkUserId(apiSecretKey, userId); } - @Operation(summary = "회원가입 정보 저장 기능", description = "회원가입 정보 저장 기능입니다.") - @PostMapping("/basic/membership/insert") - public ApiResponse membershipJoinInsert(@RequestBody JoinRequestDTO joinRequestDTO){ - - logger.info("CALL /membership/join/basic/membership/insert"); - logger.info("[input] API_SECRET_KEY={}", joinRequestDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - if(joinRequestDTO.getApiSecretKey().equals(API_SECRET_KEY)){ - - // 필수 입력값(아이디, 비밀번호, 휴대전화번호 값이 빈 값이 아닌지 확인) - // 선택 입력값(이메일, 생년월일, 성별, 닉네임) - if(inputValidate.isEmpty(joinRequestDTO.getUserId()) || inputValidate.isEmpty(joinRequestDTO.getPassword()) || inputValidate.isEmpty(joinRequestDTO.getTel())){ - - // 필수 입력값 중 빈 값이 존재. insert 중지 - resultCode = "101"; - message = "회원가입에 필요한 정보를 입력해주세요."; - - } else{ - - // 아이디, 비밀번호, 닉네임 적합성 검사 - if(!(validator.isValidId(joinRequestDTO.getUserId()) && validator.isValidPassword(joinRequestDTO.getPassword()) && validator.isValidNickname(joinRequestDTO.getNickName()))){ - resultCode = "102"; - message = "적합한 아이디, 비밀번호, 닉네임이 아닙니다."; - } else { - // 입력값 암호화 & 값 세팅 - // 휴대전화번호, 비밀번호 암호화 - joinRequestDTO.setTel(encryptUtil.encrypt(joinRequestDTO.getTel().replaceAll("[^0-9]", ""))); - - // 이메일 값이 넘어오면 암호화 - if(!inputValidate.isEmpty(joinRequestDTO.getEmail())){ - joinRequestDTO.setEmail(encryptUtil.encrypt(joinRequestDTO.getEmail())); - } - - String encodedPassword = passwordConfig.passwordEncoder().encode(joinRequestDTO.getPassword()); - joinRequestDTO.setPassword(encodedPassword); - - // 회원 정보 저장(회원가입) - JoinDTO joinDTO = new JoinDTO(); - joinDTO.setUserId(joinRequestDTO.getUserId()); - joinDTO.setPassword(joinRequestDTO.getPassword()); - joinDTO.setEmail(joinRequestDTO.getEmail()); - joinDTO.setBirth(joinRequestDTO.getBirth().replaceAll("[^0-9]", "")); - joinDTO.setTel(joinRequestDTO.getTel()); - joinDTO.setGender(joinRequestDTO.getGender()); - joinDTO.setNickName(joinRequestDTO.getNickName()); - joinDTO.setJoinType("BASIC"); - - int insertResult = joinService.insertMembershipInfo(joinDTO); - logger.info("@@ insertResult={}", insertResult); - - if(insertResult > 0){ - resultCode = "200"; - message = "회원가입에 성공하였습니다."; - } else{ - resultCode = "300"; - message = "회원가입에 실패하였습니다."; - } - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - - return apiResponse; + @Operation(summary = "회원가입 정보 저장 기능", description = "회원가입 정보 저장 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "S200", description = "회원가입에 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "S102", description = "적합한 아이디, 비밀번호, 닉네임이 아닙니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U300", description = "해당 아이디 사용이 불가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N102", description = "적합하지 않는 닉네임입니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N103", description = "해당 닉네임 사용이 불가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "S300", description = "회원가입에 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping + public ApiResponse signUp(@RequestBody JoinRequestDTO joinRequestDTO) { + return basicJoinService.signUp(joinRequestDTO); } - @Operation(summary = "닉네임 중복 체크 API", description = "닉네임 중복 체크 API입니다.") - @PostMapping("/check/duplicate/nickname") - public ApiResponse checkDuplicateNickname(@RequestBody NickNameDTO nickNameDTO){ - - logger.info("CALL /membership/join/check/duplicate/nickname"); - logger.info("[input] API_SECRET_KEY={}", nickNameDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - if(nickNameDTO.getApiSecretKey().equals(API_SECRET_KEY)){ - - // 필수 입력값 - if(inputValidate.isEmpty(nickNameDTO.getNickName())){ - - // 필수 입력값 중 빈 값이 존재. insert 중지 - resultCode = "101"; - message = "닉네임 정보를 입력해주세요."; - - } else{ - - if(!validator.isValidNickname(nickNameDTO.getNickName())) { - resultCode = "102"; - message = "적합하지 않는 닉네임입니다."; - } else { - int nickNameCnt = joinService.checkNickName(nickNameDTO); - logger.info("nickNameCnt={}", nickNameCnt); - if(nickNameCnt > 0) { - resultCode = "103"; - message = "이미 존재하는 닉네임입니다."; - } else { - resultCode = "200"; - message = "이용 가능한 닉네임입니다."; - } - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } + @Operation(summary = "닉네임 중복 체크 API", description = "닉네임 중복 체크 API", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N200", description = "사용 가능한 닉네임입니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N102", description = "적합하지 않는 닉네임입니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N103", description = "해당 닉네임 사용이 불가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping("/nickname/exists") + public ApiResponse checkDuplicateNickname(@RequestHeader("API-KEY") String apiSecretKey, @RequestParam String nickname){ + return basicJoinService.checkNickname(apiSecretKey, nickname); + } - return apiResponse; + @Operation(summary = "회원 탈퇴", description = "회원 탈퇴 API", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R301", description = "유효하지 않은 refresh token입니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R302", description = "유효한 token 정보를 찾을 수 없습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "D200", description = "회원 탈퇴되었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "D300", description = "회원 탈퇴 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @DeleteMapping("/me") + public ApiResponse deleteAccount(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO) { + return basicJoinService.deleteAccount(refreshTokenRequestDTO); } } diff --git a/src/main/java/com/barogagi/member/basic/join/dto/JoinDTO.java b/src/main/java/com/barogagi/member/basic/join/dto/JoinDTO.java index 9b41d74..4e5025a 100644 --- a/src/main/java/com/barogagi/member/basic/join/dto/JoinDTO.java +++ b/src/main/java/com/barogagi/member/basic/join/dto/JoinDTO.java @@ -34,7 +34,4 @@ public class JoinDTO extends DefaultVO { // 회원가입 종류(BASIC : 기본 / GOOGLE : 구글 / KAKAO : 카카오톡 / NAVER : 네이버) private String joinType = ""; - - // 프로필 이미지 - private String profileImg = ""; } diff --git a/src/main/java/com/barogagi/member/basic/join/dto/UserIdCheckDTO.java b/src/main/java/com/barogagi/member/basic/join/dto/UserIdCheckDTO.java deleted file mode 100644 index 1491630..0000000 --- a/src/main/java/com/barogagi/member/basic/join/dto/UserIdCheckDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.barogagi.member.basic.join.dto; - -import com.barogagi.config.vo.DefaultVO; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UserIdCheckDTO extends DefaultVO { - @NotBlank(message = "사용자 ID는 필수 입력값입니다.") - @Size(min = 4, max = 20, message = "사용자 ID는 4자 이상 20자 이하여야 합니다.") - @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "사용자 ID는 영문과 숫자만 사용 가능합니다.") - private String userId = ""; -} diff --git a/src/main/java/com/barogagi/member/basic/join/exception/JoinException.java b/src/main/java/com/barogagi/member/basic/join/exception/JoinException.java new file mode 100644 index 0000000..a82a81c --- /dev/null +++ b/src/main/java/com/barogagi/member/basic/join/exception/JoinException.java @@ -0,0 +1,13 @@ +package com.barogagi.member.basic.join.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class JoinException extends BusinessException { + + public JoinException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/barogagi/member/basic/join/mapper/JoinMapper.java b/src/main/java/com/barogagi/member/basic/join/mapper/JoinMapper.java index 4b17054..7929081 100644 --- a/src/main/java/com/barogagi/member/basic/join/mapper/JoinMapper.java +++ b/src/main/java/com/barogagi/member/basic/join/mapper/JoinMapper.java @@ -11,10 +11,10 @@ public interface JoinMapper { int insertMemberInfo(JoinDTO vo); // 아이디 중복 체크 - int checkUserId(JoinDTO vo); + int selectUserIdCnt(JoinDTO vo); // 닉네임 중복 체크 - int checkNickName(NickNameDTO dto); + int selectNicknameCnt(NickNameDTO dto); // 회원번호 중복 체크 int checkDuplicateMembershipNo(String membershipNo); diff --git a/src/main/java/com/barogagi/member/basic/join/service/BasicJoinService.java b/src/main/java/com/barogagi/member/basic/join/service/BasicJoinService.java new file mode 100644 index 0000000..60b2a03 --- /dev/null +++ b/src/main/java/com/barogagi/member/basic/join/service/BasicJoinService.java @@ -0,0 +1,222 @@ +package com.barogagi.member.basic.join.service; + +import com.barogagi.config.PasswordConfig; +import com.barogagi.member.basic.join.dto.JoinDTO; +import com.barogagi.member.basic.join.dto.JoinRequestDTO; +import com.barogagi.member.basic.join.dto.NickNameDTO; +import com.barogagi.member.basic.join.exception.JoinException; +import com.barogagi.member.login.dto.RefreshTokenRequestDTO; +import com.barogagi.member.login.service.AccountService; +import com.barogagi.member.login.service.AuthService; +import com.barogagi.response.ApiResponse; +import com.barogagi.util.EncryptUtil; +import com.barogagi.util.InputValidate; +import com.barogagi.util.Validator; +import com.barogagi.util.exception.ErrorCode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class BasicJoinService { + + private final Validator validator; + private final InputValidate inputValidate; + private final JoinService joinService; + private final AuthService authService; + private final AccountService accountService; + private final EncryptUtil encryptUtil; + private final PasswordConfig passwordConfig; + + @Autowired + public BasicJoinService( Validator validator + , InputValidate inputValidate + , JoinService joinService + , AuthService authService + , AccountService accountService + , EncryptUtil encryptUtil + , PasswordConfig passwordConfig) { + + this.validator = validator; + this.inputValidate = inputValidate; + this.joinService = joinService; + this.authService = authService; + this.accountService = accountService; + this.encryptUtil = encryptUtil; + this.passwordConfig = passwordConfig; + } + + // 닉네임 중복 체크 service + public ApiResponse checkNickname(String apiSecretKey, String nickname) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(apiSecretKey)) { + throw new JoinException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(nickname)) { + throw new JoinException(ErrorCode.EMPTY_DATA); + } + + // 3. 적합한 닉네임인지 확인 + if(!validator.isValidNickname(nickname)) { + throw new JoinException(ErrorCode.INVALID_NICKNAME); + } + + // 4. 닉네임 중복 체크 + NickNameDTO nickNameDTO = new NickNameDTO(); + nickNameDTO.setNickName(nickname); + + int nickNameCnt = joinService.selectNicknameCnt(nickNameDTO); + if(nickNameCnt > 0) { + throw new JoinException(ErrorCode.UNAVAILABLE_NICKNAME); + } + + return ApiResponse.result(ErrorCode.AVAILABLE_NICKNAME); + } + + // 아이디 중복 체크 service + public ApiResponse checkUserId(String apiSecretKey, String userId) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(apiSecretKey)) { + throw new JoinException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(userId)) { + throw new JoinException(ErrorCode.EMPTY_DATA); + } + + // 3. 적합한 아이디인지 확인 + if(!validator.isValidId(userId)) { + throw new JoinException(ErrorCode.INVALID_USER_ID); + } + + // 4. 아이디 중복 체크 + JoinDTO joinDTO = new JoinDTO(); + joinDTO.setUserId(userId); + + int checkUserId = joinService.selectUserIdCnt(joinDTO); + + if(checkUserId > 0) { + throw new JoinException(ErrorCode.UNAVAILABLE_USER_ID); + } + + return ApiResponse.result(ErrorCode.AVAILABLE_USER_ID); + } + + public ApiResponse signUp(JoinRequestDTO joinRequestDTO) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(joinRequestDTO.getApiSecretKey())) { + throw new JoinException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + // 필수 입력값(아이디, 비밀번호, 휴대전화번호 값이 빈 값이 아닌지 확인) + // 선택 입력값(이메일, 생년월일, 성별, 닉네임) + if(inputValidate.isEmpty(joinRequestDTO.getUserId()) + || inputValidate.isEmpty(joinRequestDTO.getPassword()) + || inputValidate.isEmpty(joinRequestDTO.getTel())) + { + throw new JoinException(ErrorCode.EMPTY_DATA); + } + + // 3. 적합한 아이디인지 확인 + // 아이디, 비밀번호 적합성 검사 + if(!( + validator.isValidId(joinRequestDTO.getUserId()) + && validator.isValidPassword(joinRequestDTO.getPassword())) + ) { + throw new JoinException(ErrorCode.INVALID_SIGN_UP); + } + + // 4. 암호화 + // 휴대전화번호, 비밀번호 암호화 + joinRequestDTO.setTel(encryptUtil.encrypt(joinRequestDTO.getTel().replaceAll("[^0-9]", ""))); + String encodedPassword = passwordConfig.passwordEncoder().encode(joinRequestDTO.getPassword()); + joinRequestDTO.setPassword(encodedPassword); + + // 이메일 값이 넘어오면 암호화 + if(!inputValidate.isEmpty(joinRequestDTO.getEmail())){ + joinRequestDTO.setEmail(encryptUtil.encrypt(joinRequestDTO.getEmail())); + } + + JoinDTO joinDTO = new JoinDTO(); + joinDTO.setUserId(joinRequestDTO.getUserId()); + joinDTO.setPassword(joinRequestDTO.getPassword()); + joinDTO.setEmail(joinRequestDTO.getEmail()); + + if(null != joinRequestDTO.getBirth()) { + joinRequestDTO.setBirth(joinRequestDTO.getBirth().replaceAll("[^0-9]", "")); + } + joinDTO.setBirth(joinRequestDTO.getBirth()); + joinDTO.setTel(joinRequestDTO.getTel()); + joinDTO.setGender(joinRequestDTO.getGender()); + joinDTO.setNickName(joinRequestDTO.getNickName()); + joinDTO.setJoinType("BASIC"); + + // 아이디 중복 검증 + int duplicateUserId = joinService.selectUserIdCnt(joinDTO); + + // 5. 아이디 중복 검증 + if(duplicateUserId > 0) { + throw new JoinException(ErrorCode.UNAVAILABLE_USER_ID); + } + + // 닉네임 값이 넘어올 경우 중복 검사 + if(!inputValidate.isEmpty(joinDTO.getNickName())) { + + // 닉네임 적합성 검사 + if(!validator.isValidNickname(joinRequestDTO.getNickName())) { + throw new JoinException(ErrorCode.INVALID_NICKNAME); + } + + NickNameDTO nickNameDTO = new NickNameDTO(); + nickNameDTO.setNickName(joinDTO.getNickName()); + int selectNicknameCnt = joinService.selectNicknameCnt(nickNameDTO); + + if(selectNicknameCnt > 0) { + throw new JoinException(ErrorCode.UNAVAILABLE_NICKNAME); + } + } + + // 6. 회원 정보 저장 + int insertResult = joinService.insertMembershipInfo(joinDTO); + if(insertResult <= 0){ + throw new JoinException(ErrorCode.FAIL_SIGN_UP); + } + + return ApiResponse.result(ErrorCode.SUCCESS_SIGN_UP); + } + + public ApiResponse deleteAccount(RefreshTokenRequestDTO refreshTokenRequestDTO) { + + // 1. refresh token이 공백 또는 null인지 확인 + if(inputValidate.isEmpty(refreshTokenRequestDTO.getRefreshToken())) { + throw new JoinException(ErrorCode.EMPTY_DATA); + } + + // 2. refresh token을 이용해서 membershipNo 구하기 + Map resultMap = authService.selectUserInfoByToken(refreshTokenRequestDTO.getRefreshToken()); + if(!resultMap.get("resultCode").equals("200")) { + + return ApiResponse.error( + resultMap.get("resultCode"), + resultMap.get("message") + ); + } + + int deleteResult = accountService.deleteMyAccount(resultMap.get("membershipNo")); + if(deleteResult <= 0) { + throw new JoinException(ErrorCode.FAIL_DELETE_ACCOUNT); + } + + return ApiResponse.result(ErrorCode.SUCCESS_DELETE_ACCOUNT); + } +} + + diff --git a/src/main/java/com/barogagi/member/basic/join/service/JoinService.java b/src/main/java/com/barogagi/member/basic/join/service/JoinService.java index 63bdc65..c33f06a 100644 --- a/src/main/java/com/barogagi/member/basic/join/service/JoinService.java +++ b/src/main/java/com/barogagi/member/basic/join/service/JoinService.java @@ -9,9 +9,7 @@ import org.springframework.stereotype.Service; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; @Service public class JoinService { @@ -21,55 +19,16 @@ public class JoinService { private static final SecureRandom random = new SecureRandom(); private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - @Autowired - private JoinMapper joinMapper; - - // 회원가입 정보 저장 service - public int insertMembershipInfo(JoinDTO vo) throws Exception { - - int result = 0; - - String membershipNo = ""; - - while(true) { - // 랜덤 회원번호 생성 - membershipNo = this.createRandomStr(); - - logger.info("membershipNo.isEmpty()={}", membershipNo.isEmpty()); - if(membershipNo.isEmpty()) { - break; - } - - // 회원번호 중복 체크 - boolean checkDuplicateMembershipNo = this.checkDuplicateMemberNo(membershipNo); - logger.info("checkDuplicateMembershipNo={}", checkDuplicateMembershipNo); - if(!checkDuplicateMembershipNo) { - break; - } - } + private final JoinMapper joinMapper; - logger.info("membershipNo.isEmpty()={}", membershipNo.isEmpty()); - if(!membershipNo.isEmpty()) { // 회원번호가 비어있을 경우 저장 X - vo.setMembershipNo(membershipNo); - result = this.insertMemberInfo(vo); - } - - return result; - } - - // 회원가입 정보 저장 기능 - public int insertMemberInfo(JoinDTO vo) throws Exception{ - return joinMapper.insertMemberInfo(vo); - } - - // 아이디 중복 체크 - public int checkUserId(JoinDTO vo) throws Exception{ - return joinMapper.checkUserId(vo); + @Autowired + public JoinService(JoinMapper joinMapper) { + this.joinMapper = joinMapper; } - // 닉네임 중복 체크 - public int checkNickName(NickNameDTO nickNameDTO) throws Exception{ - return joinMapper.checkNickName(nickNameDTO); + // 닉네임 개수 구하기 + public int selectNicknameCnt(NickNameDTO nickNameDTO) { + return joinMapper.selectNicknameCnt(nickNameDTO); } // 회원번호 랜덤값 생성 @@ -110,7 +69,7 @@ public String createRandomStr() { } // 회원번호 중복 체크 - public boolean checkDuplicateMemberNo(String membershipNo) throws Exception { + public boolean checkDuplicateMemberNo(String membershipNo) { boolean duplicateFlag = false; int membershipNoCnt = joinMapper.checkDuplicateMembershipNo(membershipNo); @@ -122,4 +81,47 @@ public boolean checkDuplicateMemberNo(String membershipNo) throws Exception { return duplicateFlag; } + + // 회원가입 정보 저장 service + public int insertMembershipInfo(JoinDTO vo) { + + int result = 0; + + String membershipNo = ""; + + while(true) { + // 랜덤 회원번호 생성 + membershipNo = this.createRandomStr(); + + logger.info("membershipNo.isEmpty()={}", membershipNo.isEmpty()); + if(membershipNo.isEmpty()) { + break; + } + + // 회원번호 중복 체크 + boolean checkDuplicateMembershipNo = this.checkDuplicateMemberNo(membershipNo); + logger.info("checkDuplicateMembershipNo={}", checkDuplicateMembershipNo); + if(!checkDuplicateMembershipNo) { + break; + } + } + + logger.info("membershipNo.isEmpty()={}", membershipNo.isEmpty()); + if(!membershipNo.isEmpty()) { // 회원번호가 비어있을 경우 저장 X + vo.setMembershipNo(membershipNo); + result = this.insertMemberInfo(vo); + } + + return result; + } + + // 회원가입 정보 저장 기능 + public int insertMemberInfo(JoinDTO vo) { + return joinMapper.insertMemberInfo(vo); + } + + // 아이디 개수 구하기 + public int selectUserIdCnt(JoinDTO vo) { + return joinMapper.selectUserIdCnt(vo); + } } diff --git a/src/main/java/com/barogagi/member/info/MemberInfoException.java b/src/main/java/com/barogagi/member/info/MemberInfoException.java deleted file mode 100644 index cc9b2ba..0000000 --- a/src/main/java/com/barogagi/member/info/MemberInfoException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.barogagi.member.info; - -import lombok.Getter; - -@Getter -public class MemberInfoException extends RuntimeException{ - private final String resultCode; - - public MemberInfoException(String resultCode, String message) { - super(message); - this.resultCode = resultCode; - } -} diff --git a/src/main/java/com/barogagi/member/info/controller/InfoController.java b/src/main/java/com/barogagi/member/info/controller/InfoController.java index 4f6a822..4a9a064 100644 --- a/src/main/java/com/barogagi/member/info/controller/InfoController.java +++ b/src/main/java/com/barogagi/member/info/controller/InfoController.java @@ -1,200 +1,47 @@ package com.barogagi.member.info.controller; -import com.barogagi.config.PasswordConfig; -import com.barogagi.member.basic.join.dto.NickNameDTO; -import com.barogagi.member.basic.join.service.JoinService; -import com.barogagi.member.info.MemberInfoException; -import com.barogagi.member.info.dto.Member; import com.barogagi.member.info.dto.MemberRequestDTO; import com.barogagi.member.info.service.MemberService; import com.barogagi.response.ApiResponse; -import com.barogagi.util.EncryptUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @Tag(name = "회원 정보", description = "회원 정보 관련 API") @RestController -@RequestMapping("/info") +@RequestMapping("/api/v1/members") public class InfoController { - private static final Logger logger = LoggerFactory.getLogger(InfoController.class); - private final MemberService memberService; - private final JoinService joinService; - - private final EncryptUtil encryptUtil; - - private final PasswordConfig passwordConfig; - - public InfoController(MemberService memberService, - JoinService joinService, - EncryptUtil encryptUtil, - PasswordConfig passwordConfig) { + public InfoController(MemberService memberService) { this.memberService = memberService; - this.joinService = joinService; - this.encryptUtil = encryptUtil; - this.passwordConfig = passwordConfig; } - @Operation(summary = "회원 정보 조회", description = "회원 정보 조회 기능입니다.") - @PostMapping("/member") + @Operation(summary = "회원 정보 조회", description = "회원 정보 조회 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A401", description = "접근 권한이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L102", description = "회원 정보가 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M200", description = "회원 정보 조회가 완료되었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping public ApiResponse selectMemberInfo(HttpServletRequest request) { - logger.info("CALL /info/member"); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - String membershipNo = String.valueOf(request.getAttribute("membershipNo")); - logger.info("@@ membershipNo.isEmpty()={}", membershipNo.isEmpty()); - if (membershipNo.isEmpty()) { - throw new MemberInfoException("401", "접근 권한이 존재하지 않습니다."); - } - - // 회원 정보 조회 - Member memberInfo = memberService.findByMembershipNo(membershipNo); - if(null == memberInfo) { - throw new MemberInfoException("402", "해당 사용자에 대한 정보가 존재하지 않습니다."); - } - - // 이메일 복호화 - memberInfo.setEmail(encryptUtil.decrypt(memberInfo.getEmail())); - - // 전화번호 복호화 - memberInfo.setTel(encryptUtil.decrypt(memberInfo.getTel())); - - // 비밀번호는 보내주지 않는다 - memberInfo.setPassword(""); - - resultCode = "200"; - message = "회원 정보 조회가 완료되었습니다."; - apiResponse.setData(memberInfo); - - } catch (MemberInfoException ex) { - resultCode = ex.getResultCode(); - message = ex.getMessage(); - - } catch (Exception e) { - logger.error("error", e); - resultCode = "400"; - message = "오류가 발생하였습니다."; - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - - return apiResponse; + return memberService.selectMemberInfo(request); } - @Operation(summary = "회원 정보 조회", description = "회원 정보 조회 기능입니다.") - @PostMapping("/member/update") - public ApiResponse updateMemberInfo(HttpServletRequest request, @RequestBody MemberRequestDTO memberRequestDto) { - logger.info("CALL /info/member/update"); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - - logger.info("param password={}", memberRequestDto.getPassword()); - logger.info("param email={}", memberRequestDto.getEmail()); - logger.info("param gender={}", memberRequestDto.getGender()); - logger.info("param nickName={}", memberRequestDto.getNickName()); - logger.info("param tel={}", memberRequestDto.getTel()); - - String membershipNo = String.valueOf(request.getAttribute("membershipNo")); - logger.info("@@ membershipNo.isEmpty()={}", membershipNo.isEmpty()); - if (membershipNo.isEmpty()) { - throw new MemberInfoException("401", "접근 권한이 존재하지 않습니다."); - } - - // 회원 정보 조회 - Member memberInfo = memberService.findByMembershipNo(membershipNo); - if(null == memberInfo) { - throw new MemberInfoException("402", "해당 사용자에 대한 정보가 존재하지 않습니다."); - } - - String joinType = memberInfo.getJoinType(); - logger.info("@@ joinType={}", joinType); - if(joinType.equals("BASIC")) { - // 일반 회원가입으로 가입한 경우에만 비밀번호 수정 가능 - if(!memberRequestDto.getPassword().isEmpty()) { - String encodedPassword = passwordConfig.passwordEncoder().encode(memberRequestDto.getPassword()); - memberInfo.setPassword(encodedPassword); - } - } - - // 이메일 - if(!memberRequestDto.getEmail().isEmpty()) { - String encodedEmail = encryptUtil.encrypt(memberRequestDto.getEmail()); - memberInfo.setEmail(encodedEmail); - } - - // 생년월일 - if(!memberRequestDto.getBirth().isEmpty()) { - memberInfo.setBirth(memberRequestDto.getBirth().replaceAll("[^0-9]", "")); - } - - // 성별 (M : 남 / W : 여) - if(!memberRequestDto.getGender().isEmpty()) { - memberInfo.setGender(memberRequestDto.getGender()); - } - - // 닉네임(중복X) - if(!memberRequestDto.getNickName().isEmpty()) { - NickNameDTO nickNameRequest = new NickNameDTO(); - nickNameRequest.setNickName(memberRequestDto.getNickName()); - int nickNameCnt = joinService.checkNickName(nickNameRequest); - - logger.info("@@ nickNameCnt={}", nickNameCnt); - if(nickNameCnt > 0) { - throw new MemberInfoException("403", "이미 해당 닉네임이 존재합니다."); - } - - memberInfo.setNickName(memberRequestDto.getNickName()); - } - - // 프로필 이미지(저장 코드는 회의 진행 후 작업) - - // 전화번호 - if(!memberRequestDto.getTel().isEmpty()) { - memberInfo.setTel(encryptUtil.encrypt(memberRequestDto.getTel().replaceAll("[^0-9]", ""))); - } - - int updateMemberInfo = memberService.updateMemberInfo(memberInfo); - logger.info("@@ updateMemberInfo={}", updateMemberInfo); - if(updateMemberInfo <= 0) { - throw new MemberInfoException("404", "사용자 정보 수정 실패하였습니다."); - } - - // 사용자 정보 수정 성공 - resultCode = "200"; - message = "사용자 정보 수정 완료하였습니다."; - - } catch (MemberInfoException ex) { - resultCode = ex.getResultCode(); - message = ex.getMessage(); - - } catch (Exception e) { - logger.error("error", e); - resultCode = "400"; - message = "오류가 발생하였습니다."; - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - - return apiResponse; + @Operation(summary = "회원 정보 수정", description = "회원 정보 조회 수정입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A401", description = "접근 권한이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L102", description = "회원 정보가 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "N103", description = "해당 닉네임 사용이 불가능합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M404", description = "사용자 정보 수정 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "M200", description = "사용자 정보 수정 완료하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PatchMapping + public ApiResponse updateMemberInfo(HttpServletRequest request, @RequestBody MemberRequestDTO memberRequestDTO) { + return memberService.updateMemberProcess(request, memberRequestDTO); } } diff --git a/src/main/java/com/barogagi/member/info/dto/Member.java b/src/main/java/com/barogagi/member/info/dto/Member.java index 917725f..3402513 100644 --- a/src/main/java/com/barogagi/member/info/dto/Member.java +++ b/src/main/java/com/barogagi/member/info/dto/Member.java @@ -34,9 +34,6 @@ public class Member { // 회원가입 종류(BASIC : 기본 / GOOGLE : 구글 / KAKAO : 카카오톡 / NAVER : 네이버) private String joinType = ""; - // 프로필 이미지 - private String profileImg = ""; - // 등록일 private String regDate = ""; diff --git a/src/main/java/com/barogagi/member/info/dto/MemberRequestDTO.java b/src/main/java/com/barogagi/member/info/dto/MemberRequestDTO.java index 90c68c7..dbf2f89 100644 --- a/src/main/java/com/barogagi/member/info/dto/MemberRequestDTO.java +++ b/src/main/java/com/barogagi/member/info/dto/MemberRequestDTO.java @@ -6,13 +6,7 @@ @Getter @Setter -public class MemberRequestDTO extends DefaultVO { - - // 비밀번호 - private String password = ""; - - // 이메일 - private String email = ""; +public class MemberRequestDTO { // 생년월일 private String birth = ""; @@ -22,10 +16,4 @@ public class MemberRequestDTO extends DefaultVO { // 닉네임 private String nickName = ""; - - // 프로필 이미지 - private String profileImg = ""; - - // 휴대폰 번호 - private String tel = ""; } diff --git a/src/main/java/com/barogagi/member/info/exception/MemberInfoException.java b/src/main/java/com/barogagi/member/info/exception/MemberInfoException.java new file mode 100644 index 0000000..952c4d3 --- /dev/null +++ b/src/main/java/com/barogagi/member/info/exception/MemberInfoException.java @@ -0,0 +1,13 @@ +package com.barogagi.member.info.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class MemberInfoException extends BusinessException { + + public MemberInfoException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/barogagi/member/info/service/MemberService.java b/src/main/java/com/barogagi/member/info/service/MemberService.java index c791062..1df0fe4 100644 --- a/src/main/java/com/barogagi/member/info/service/MemberService.java +++ b/src/main/java/com/barogagi/member/info/service/MemberService.java @@ -1,25 +1,139 @@ package com.barogagi.member.info.service; +import com.barogagi.member.basic.join.dto.NickNameDTO; +import com.barogagi.member.basic.join.service.JoinService; import com.barogagi.member.info.dto.Member; +import com.barogagi.member.info.dto.MemberRequestDTO; +import com.barogagi.member.info.exception.MemberInfoException; import com.barogagi.member.info.mapper.MemberMapper; +import com.barogagi.response.ApiResponse; +import com.barogagi.util.EncryptUtil; +import com.barogagi.util.InputValidate; +import com.barogagi.util.MembershipUtil; +import com.barogagi.util.exception.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Map; + @Service public class MemberService { + private final MemberMapper memberMapper; + private final MembershipUtil membershipUtil; + private final EncryptUtil encryptUtil; + private final JoinService joinService; + private final InputValidate inputValidate; + @Autowired - private MemberMapper memberMapper; + public MemberService(MemberMapper memberMapper, + MembershipUtil membershipUtil, + EncryptUtil encryptUtil, + JoinService joinService, + InputValidate inputValidate) + { + this.memberMapper = memberMapper; + this.membershipUtil = membershipUtil; + this.encryptUtil = encryptUtil; + this.joinService = joinService; + this.inputValidate = inputValidate; + } + + public ApiResponse selectMemberInfo(HttpServletRequest request) { + + // 1. 회원번호 구하기 + Map membershipNoInfo = membershipUtil.membershipNoService(request); + if(!membershipNoInfo.get("resultCode").equals("A200")) { + return ApiResponse.error( + String.valueOf(membershipNoInfo.get("resultCode")), + String.valueOf(membershipNoInfo.get("message")) + ); + } + String membershipNo = String.valueOf(membershipNoInfo.get("membershipNo")); + + // 2. 회원 정보 조회 + Member memberInfo = this.findByMembershipNo(membershipNo); + if(null == memberInfo) { + throw new MemberInfoException(ErrorCode.NOT_FOUND_USER_INFO); + } + + // 이메일 복호화 + memberInfo.setEmail(encryptUtil.decrypt(memberInfo.getEmail())); + + // 전화번호 복호화 + memberInfo.setTel(encryptUtil.decrypt(memberInfo.getTel())); + + // 비밀번호는 보내주지 않는다 + memberInfo.setPassword(""); + + return ApiResponse.resultData( + memberInfo, + ErrorCode.FOUND_USER_INFO.getCode(), + ErrorCode.FOUND_USER_INFO.getMessage() + ); + } + + public ApiResponse updateMemberProcess(HttpServletRequest request, MemberRequestDTO memberRequestDTO) { + + // 1. 회원번호 구하기 + Map membershipNoInfo = membershipUtil.membershipNoService(request); + if(!membershipNoInfo.get("resultCode").equals("A200")) { + return ApiResponse.error( + String.valueOf(membershipNoInfo.get("resultCode")), + String.valueOf(membershipNoInfo.get("message")) + ); + } + + String membershipNo = String.valueOf(membershipNoInfo.get("membershipNo")); + + // 2. 회원 정보 조회 + Member memberInfo = this.findByMembershipNo(membershipNo); + if(null == memberInfo) { + throw new MemberInfoException(ErrorCode.NOT_FOUND_USER_INFO); + } + + // 3. 데이터 처리 + // 생년월일 + if(!inputValidate.isEmpty(memberRequestDTO.getBirth())) { + memberInfo.setBirth(memberRequestDTO.getBirth().replaceAll("[^0-9]", "")); + } + + // 성별 (M : 남 / W : 여) + if(!inputValidate.isEmpty(memberRequestDTO.getGender())) { + memberInfo.setGender(memberRequestDTO.getGender()); + } + + // 닉네임(중복X) + if(!inputValidate.isEmpty(memberRequestDTO.getNickName())) { + NickNameDTO nickNameRequest = new NickNameDTO(); + nickNameRequest.setNickName(memberRequestDTO.getNickName()); + int nickNameCnt = joinService.selectNicknameCnt(nickNameRequest); + + if(nickNameCnt > 0) { + throw new MemberInfoException(ErrorCode.UNAVAILABLE_NICKNAME); + } + + memberInfo.setNickName(memberRequestDTO.getNickName()); + } + + int updateMemberInfo = this.updateMemberInfo(memberInfo); + if(updateMemberInfo <= 0) { + throw new MemberInfoException(ErrorCode.FAIL_UPDATE_USER_INFO); + } + + return ApiResponse.result(ErrorCode.SUCCESS_UPDATE_USER_INFO); + } - public Member findByMembershipNo(String membershipNo) throws Exception { + public Member findByMembershipNo(String membershipNo) { return memberMapper.findByMembershipNo(membershipNo); } - public Member selectUserMembershipInfo(String userId) throws Exception { + public Member selectUserMembershipInfo(String userId) { return memberMapper.selectUserMembershipInfo(userId); } - public int updateMemberInfo(Member member) throws Exception { + public int updateMemberInfo(Member member) { return memberMapper.updateMemberInfo(member); } } diff --git a/src/main/java/com/barogagi/member/login/controller/AuthController.java b/src/main/java/com/barogagi/member/login/controller/AuthController.java index 69da475..e69de29 100644 --- a/src/main/java/com/barogagi/member/login/controller/AuthController.java +++ b/src/main/java/com/barogagi/member/login/controller/AuthController.java @@ -1,103 +0,0 @@ -package com.barogagi.member.login.controller; - -import com.barogagi.member.login.dto.*; -import com.barogagi.member.login.exception.InvalidRefreshTokenException; -import com.barogagi.member.login.service.AccountService; -import com.barogagi.member.login.service.AuthService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; -import java.util.Optional; - -@Tag(name = "TOKEN 재발급, 로그아웃, 탈퇴", description = "TOKEN 재발급, 로그아웃, 탈퇴 관련 API") -@RestController -@RequestMapping("/auth") -public class AuthController { - - private static final Logger logger = LoggerFactory.getLogger(AuthController.class); - - private final AuthService authService; - private final AccountService accountService; - - public AuthController(AuthService authService, AccountService accountService) { - this.authService = authService; - this.accountService = accountService; - } - - @Operation(summary = "토큰 재발급", description = "Access 토큰 만료 시, Refresh 토큰으로 Access 토큰 재발급") - @PostMapping("/refresh") - public ResponseEntity> refresh( - @RequestHeader(value = "X-Refresh-Token", required = false) String refreshHeader, - @RequestBody(required = false) Map body, - HttpServletResponse response - ) { - - logger.info("CALL /auth/refresh"); - - try { - String rt = Optional.ofNullable(refreshHeader) - .or(() -> Optional.ofNullable(body == null ? null : body.get("refreshToken"))) - .orElse(null); - - if (rt == null || rt.isBlank()) { - return ResponseEntity.status(401).body(Map.of("error", "refresh_required")); - } - - TokenPair pair = authService.rotate(rt); // ❗️핵심 로직 (아래 2) 참조) - - return ResponseEntity.ok(Map.of( - "accessToken", pair.accessToken(), - "accessTokenExpiresIn", pair.accessTokenExpiresIn(), - "refreshToken", pair.refreshToken(), - "refreshTokenExpiresIn", pair.refreshTokenExpiresIn() - )); - - } catch (InvalidRefreshTokenException e) { - return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED) - .body(Map.of( - "resultCode", "400", - "errorCode", e.getCode(), - "message", e.getMessage(), - "needLogin", true - )); - } - } - - /** - * 현재 기기 로그아웃: 전달된 refreshToken이 속한 (membershipNo, deviceId)의 VALID 토큰들을 REVOKE - * 입력 경로: - * - 헤더: X-Refresh-Token: - * - 바디: { "refreshToken": "" } - */ - @Operation(summary = "현재 기기 로그아웃", description = "현재 기기 로그아웃: 전달된 refreshToken이 속한 (membershipNo, deviceId)의 VALID 토큰들을 REVOKE") - @PostMapping("/logout") - public ResponseEntity> logout( - @RequestHeader(value = "X-Refresh-Token", required = false) String refreshHeader, - @RequestBody(required = false) Map body - ) { - String refresh = refreshHeader; - if ((refresh == null || refresh.isBlank()) && body != null) { - refresh = body.get("refreshToken"); - } - if (refresh != null && !refresh.isBlank()) { - authService.logout(refresh); // DB REVOKE - } - return ResponseEntity.ok(Map.of("result", "logged_out")); - } - - @DeleteMapping - public ResponseEntity deleteMe(Authentication auth) { - String membershipNo = (String) auth.getPrincipal(); // JwtAuthFilter에서 세팅됨 - accountService.deleteMyAccount(membershipNo); - return ResponseEntity.noContent().build(); // 204 - } -} - - diff --git a/src/main/java/com/barogagi/member/login/controller/LoginController.java b/src/main/java/com/barogagi/member/login/controller/LoginController.java index 4ae4162..829ca99 100644 --- a/src/main/java/com/barogagi/member/login/controller/LoginController.java +++ b/src/main/java/com/barogagi/member/login/controller/LoginController.java @@ -1,251 +1,87 @@ package com.barogagi.member.login.controller; -import com.barogagi.config.PasswordConfig; -import com.barogagi.member.info.dto.Member; -import com.barogagi.member.info.service.MemberService; import com.barogagi.member.login.dto.*; -import com.barogagi.member.login.exception.LoginException; -import com.barogagi.member.login.service.AuthService; import com.barogagi.member.login.service.LoginService; import com.barogagi.response.ApiResponse; -import com.barogagi.util.EncryptUtil; -import com.barogagi.util.InputValidate; -import com.barogagi.util.JwtUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Tag(name = "일반 로그인", description = "일반 로그인 관련 API") +@Tag(name = "일반 로그인 & 토큰 재발급", description = "일반 로그인, 토큰 재발급 관련 API") @RestController -@RequestMapping("/login") +@RequestMapping("/api/v1/auth") public class LoginController { - private static final Logger logger = LoggerFactory.getLogger(LoginController.class); - - private final InputValidate inputValidate; - private final PasswordConfig passwordConfig; - - private final EncryptUtil encryptUtil; - private final JwtUtil jwtUtil; private final LoginService loginService; - private final MemberService memberService; - private final AuthService authService; - - private final PasswordEncoder passwordEncoder; - - private final String API_SECRET_KEY; @Autowired - public LoginController(Environment environment, - InputValidate inputValidate, - EncryptUtil encryptUtil, - LoginService loginService, - MemberService memberService, - AuthService authService, - JwtUtil jwtUtil, - PasswordConfig passwordConfig, - PasswordEncoder passwordEncoder){ - this.API_SECRET_KEY = environment.getProperty("api.secret-key"); - - this.inputValidate = inputValidate; - this.encryptUtil = encryptUtil; + public LoginController(LoginService loginService){ this.loginService = loginService; - this.memberService = memberService; - this.authService = authService; - this.jwtUtil = jwtUtil; - this.passwordConfig = passwordConfig; - this.passwordEncoder = passwordEncoder; } - @Operation(summary = "로그인", description = "로그인 기능입니다. apiSecretKey, userId와 password 값만 보내주세요.") - @PostMapping("/basic/membership/login") + @Operation(summary = "로그인", description = "로그인 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R200", description = "로그인에 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L102", description = "회원 정보가 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L103", description = "로그인에 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/login") public ApiResponse basicMemberLogin(@RequestBody LoginDTO loginRequestDTO){ - - logger.info("CALL /login/basic/membership/login"); - logger.info("[input] API_SECRET_KEY={}", loginRequestDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if (loginRequestDTO.getApiSecretKey().equals(API_SECRET_KEY)) { - - if (inputValidate.isEmpty(loginRequestDTO.getUserId()) || inputValidate.isEmpty(loginRequestDTO.getPassword())) { - throw new LoginException("101", "로그인이 불가능합니다."); - } else { - - logger.info("@@@ userId={}", loginRequestDTO.getUserId()); - logger.info("@@@ password={}", loginRequestDTO.getPassword()); - - // 로그인 성공 -> 사용자 정보 조회(membershipNo, userId 등 토큰에 넣을 값) - Member member = memberService.selectUserMembershipInfo(loginRequestDTO.getUserId()); - if (null == member) { - throw new LoginException("102", "회원 정보가 존재하지 않습니다."); - } - - boolean ok = passwordEncoder.matches(loginRequestDTO.getPassword(), member.getPassword()); - - logger.info("@@ ok={}", ok); - if(!ok) { - throw new LoginException("103", "로그인에 실패하였습니다"); - } - - String userId = member.getUserId(); - - // ACCESS, REFRESH TOKEN 생싱 & REFRESH TOKEN 저장 - LoginResponse loginResponse = authService.loginAfterSignup(userId, "web-basic"); - Map loginResponseMap = Map.of( - "accessToken", loginResponse.tokens().accessToken(), - "accessTokenExpiresIn", loginResponse.tokens().accessTokenExpiresIn(), - "userId", userId, - "membershipNo", loginResponse.membershipNo(), - "refreshToken", loginResponse.tokens().refreshToken(), - "refreshTokenExpiresIn", loginResponse.tokens().refreshTokenExpiresIn() - ); - - resultCode = "200"; - message = "로그인에 성공하였습니다."; - apiResponse.setData(loginResponseMap); - } - - } else { - throw new LoginException("100", "잘못된 접근입니다."); - } - } catch (LoginException ex) { - resultCode = ex.getCode(); - message = ex.getMessage(); - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + return loginService.login(loginRequestDTO); } - @Operation(summary = "아이디 찾기 기능", description = "아이디 찾기 기능입니다. apiSecretKey, tel 값만 보내주시면 됩니다.") - @PostMapping("/basic/membership/userId/search") - public ApiResponse searchUserId(@RequestBody SearchUserIdDTO searchUserIdRequestDTO){ - - logger.info("CALL /login/basic/membership/userId/search"); - logger.info("[input] API_SECRET_KEY={}", searchUserIdRequestDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if(searchUserIdRequestDTO.getApiSecretKey().equals(API_SECRET_KEY)){ - - if(inputValidate.isEmpty(searchUserIdRequestDTO.getTel())){ - resultCode = "101"; - message = "전화번호가 존재하지 않습니다."; - - } else { - - searchUserIdRequestDTO.setTel(encryptUtil.encrypt(searchUserIdRequestDTO.getTel())); - logger.info("tel={}", searchUserIdRequestDTO.getTel()); - List myUserIdList = loginService.myUserIdList(searchUserIdRequestDTO); - - int userIdCnt = myUserIdList.size(); - if(userIdCnt > 0){ - resultCode = "200"; - message = "해당 전화번호로 가입된 아이디입니다."; - - List> userIdList = new ArrayList<>(); - for(UserIdDTO vo : myUserIdList){ - Map map = new HashMap<>(); - map.put("userId", vo.getUserId()); - userIdList.add(map); - } - apiResponse.setData(userIdList); - - } else { - resultCode = "201"; - message = "해당 전화번호로 가입된 계정이 존재하지 않습니다."; - } - } - - } else{ - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + @Operation(summary = "아이디 찾기 기능", description = "아이디 찾기 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "F200", description = "해당 전화번호로 가입된 아이디가 존재합니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "F201", description = "해당 전화번호로 가입된 계정이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/find-user") + public ApiResponse findUser(@RequestBody SearchUserIdDTO searchUserIdRequestDTO){ + return loginService.findUser(searchUserIdRequestDTO); } - @Operation(summary = "비밀번호 재설정 기능", description = "비밀번호 재설정 기능입니다. apiSecretKey, userId, password값만 보내주시면 됩니다.") - @PostMapping("/basic/membership/password/update") + @Operation(summary = "비밀번호 재설정 기능", description = "비밀번호 재설정 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U200", description = "비밀번호 재설정에 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "U300", description = "비밀번호 재설정에 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/password-reset/confirm") public ApiResponse updatePassword(@RequestBody LoginDTO vo){ + return loginService.updatePasswordProcess(vo); + } - logger.info("CALL /login/basic/membership/password/update"); - logger.info("[input] API_SECRET_KEY={}", vo.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if(vo.getApiSecretKey().equals(API_SECRET_KEY)){ - - if(inputValidate.isEmpty(vo.getUserId()) || inputValidate.isEmpty(vo.getPassword())){ - resultCode = "101"; - message = "아이디, 비밀번호 값이 없습니다."; - - } else { - // 비밀번호 암호화 - vo.setPassword(encryptUtil.hashEncodeString(vo.getPassword())); - - // 비밀번호 update - int updatePassword = loginService.updatePassword(vo); - logger.info("@@ updatePassword={}", updatePassword); - - if(updatePassword > 0){ - resultCode = "200"; - message = "비밀번호 재설정에 성공하였습니다."; - } else{ - resultCode = "300"; - message = "비밀번호 재설정에 실패하였습니다."; - } - } - - } else{ - resultCode = "100"; - message = "잘못된 접근입니다."; - } + @Operation(summary = "토큰 재발급", description = "Access 토큰 만료 시, Refresh 토큰으로 Access 토큰 재발급", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R110", description = "로그인을 진행해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R120", description = "로그인을 다시 진행해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L102", description = "회원 정보가 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "R200", description = "토큰이 발급되었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/token/refresh") + public ApiResponse refresh(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO) { + return loginService.refreshToken(refreshTokenRequestDTO); + } - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - return apiResponse; + @Operation(summary = "현재 기기 로그아웃", description = "현재 기기 로그아웃: 전달된 refreshToken이 속한 (membershipNo, deviceId)의 VALID 토큰들을 REVOKE", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L200", description = "로그아웃 되었습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/logout") + public ApiResponse logout(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO) { + return loginService.logout(refreshTokenRequestDTO); } } diff --git a/src/main/java/com/barogagi/member/login/dto/RefreshTokenRequestDTO.java b/src/main/java/com/barogagi/member/login/dto/RefreshTokenRequestDTO.java new file mode 100644 index 0000000..9a573d4 --- /dev/null +++ b/src/main/java/com/barogagi/member/login/dto/RefreshTokenRequestDTO.java @@ -0,0 +1,12 @@ +package com.barogagi.member.login.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RefreshTokenRequestDTO { + + private String refreshToken = ""; + +} diff --git a/src/main/java/com/barogagi/member/login/dto/TokenPair.java b/src/main/java/com/barogagi/member/login/dto/TokenPair.java index 7554a95..8e3da4e 100644 --- a/src/main/java/com/barogagi/member/login/dto/TokenPair.java +++ b/src/main/java/com/barogagi/member/login/dto/TokenPair.java @@ -2,5 +2,6 @@ public record TokenPair( String accessToken, long accessTokenExpiresIn, - String refreshToken, long refreshTokenExpiresIn + String refreshToken, long refreshTokenExpiresIn, + String resultCode, String message ) {} diff --git a/src/main/java/com/barogagi/member/login/entity/UserMembership.java b/src/main/java/com/barogagi/member/login/entity/UserMembership.java index f62d245..a91478c 100644 --- a/src/main/java/com/barogagi/member/login/entity/UserMembership.java +++ b/src/main/java/com/barogagi/member/login/entity/UserMembership.java @@ -34,9 +34,6 @@ public class UserMembership { @Column(name = "GENDER", length = 1) private String gender; // M / W - @Column(name = "PROFILE_IMG", length = 200) - private String profileImg; - @Column(name = "NICKNAME", length = 100) private String nickname; diff --git a/src/main/java/com/barogagi/member/login/exception/InvalidRefreshTokenException.java b/src/main/java/com/barogagi/member/login/exception/InvalidRefreshTokenException.java index c330e3e..5c4c491 100644 --- a/src/main/java/com/barogagi/member/login/exception/InvalidRefreshTokenException.java +++ b/src/main/java/com/barogagi/member/login/exception/InvalidRefreshTokenException.java @@ -1,14 +1,13 @@ package com.barogagi.member.login.exception; +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; import lombok.Getter; @Getter -public class InvalidRefreshTokenException extends RuntimeException{ +public class InvalidRefreshTokenException extends BusinessException { - private final String code; - - public InvalidRefreshTokenException(String code, String message) { - super(message); - this.code = code; + public InvalidRefreshTokenException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/src/main/java/com/barogagi/member/login/exception/LoginException.java b/src/main/java/com/barogagi/member/login/exception/LoginException.java index 3dda611..ca5c843 100644 --- a/src/main/java/com/barogagi/member/login/exception/LoginException.java +++ b/src/main/java/com/barogagi/member/login/exception/LoginException.java @@ -1,13 +1,13 @@ package com.barogagi.member.login.exception; +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; import lombok.Getter; @Getter -public class LoginException extends RuntimeException { - private final String code; +public class LoginException extends BusinessException { - public LoginException(String code, String message) { - super(message); - this.code = code; + public LoginException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/src/main/java/com/barogagi/member/login/mapper/AuthMapper.java b/src/main/java/com/barogagi/member/login/mapper/AuthMapper.java new file mode 100644 index 0000000..ea6005b --- /dev/null +++ b/src/main/java/com/barogagi/member/login/mapper/AuthMapper.java @@ -0,0 +1,9 @@ +package com.barogagi.member.login.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AuthMapper { + + String selectUserInfoByToken(String refreshToken); +} diff --git a/src/main/java/com/barogagi/member/login/service/AccountService.java b/src/main/java/com/barogagi/member/login/service/AccountService.java index b199387..c473b7f 100644 --- a/src/main/java/com/barogagi/member/login/service/AccountService.java +++ b/src/main/java/com/barogagi/member/login/service/AccountService.java @@ -14,10 +14,20 @@ public class AccountService { private final UserMembershipRepository userMembershipRepository; @Transactional - public void deleteMyAccount(String membershipNo) { + public int deleteMyAccount(String membershipNo) { + + int result = 0; + // 1) 모든 리프레시 토큰 제거 - refreshTokenRepository.deleteAllByMembershipNo(membershipNo); + int deleteRefreshToken = refreshTokenRepository.deleteAllByMembershipNo(membershipNo); + // 2) 회원 삭제 - userMembershipRepository.deleteByMembershipNo(membershipNo); + int deleteByMembershipNo = userMembershipRepository.deleteByMembershipNo(membershipNo); + + if(deleteRefreshToken > 0 && deleteByMembershipNo > 0) { + result = 1; + } + + return result; } } diff --git a/src/main/java/com/barogagi/member/login/service/AuthService.java b/src/main/java/com/barogagi/member/login/service/AuthService.java index 922943e..8e733e8 100644 --- a/src/main/java/com/barogagi/member/login/service/AuthService.java +++ b/src/main/java/com/barogagi/member/login/service/AuthService.java @@ -4,9 +4,11 @@ import com.barogagi.member.login.entity.RefreshToken; import com.barogagi.member.login.entity.UserMembership; import com.barogagi.member.login.exception.InvalidRefreshTokenException; +import com.barogagi.member.login.mapper.AuthMapper; import com.barogagi.member.login.repository.RefreshTokenRepository; import com.barogagi.member.login.repository.UserMembershipRepository; import com.barogagi.util.JwtUtil; +import com.barogagi.util.exception.ErrorCode; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; @@ -14,7 +16,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,17 +34,21 @@ public class AuthService { private final JwtUtil jwt; private final PasswordEncoder encoder; + private final AuthMapper authMapper; + @Value("${jwt.access-exp-seconds}") private long accessExp; @Value("${jwt.refresh-exp-seconds}") private long refreshExp; public AuthService(UserMembershipRepository userRepo, RefreshTokenRepository refreshRepo, - JwtUtil jwt, PasswordEncoder encoder) { + JwtUtil jwt, PasswordEncoder encoder, + AuthMapper authMapper) { this.userRepo = userRepo; this.refreshRepo = refreshRepo; this.jwt = jwt; this.encoder = encoder; + this.authMapper = authMapper; } public LoginResponse login(LoginRequest req) { @@ -68,27 +77,29 @@ public LoginResponse login(LoginRequest req) { refreshRepo.save(rt); return new LoginResponse( - new TokenPair(access, accessExp, refresh, refreshExp), + new TokenPair( + access, + accessExp, + refresh, + refreshExp, + ErrorCode.SUCCESS_LOGIN.getCode(), + ErrorCode.SUCCESS_LOGIN.getMessage() + ), no, u.getUserId(), u.getJoinType() ); } /** 구글/네이버 등 OAuth 가입 직후: userId로 바로 토큰 발급 (비밀번호 검증 없음) */ public LoginResponse loginAfterSignup(String userId, String deviceId) { - var u = userRepo.findByUserId(userId) + UserMembership u = userRepo.findByUserId(userId) .orElseThrow(() -> new RuntimeException("USER_NOT_FOUND")); - // 선택: BASIC 사용자가 이 엔드포인트를 타지 못하게 막고 싶다면 -// if ("BASIC".equalsIgnoreCase(u.getJoinType())) { -// throw new RuntimeException("NOT_OAUTH_MEMBER"); -// } - String no = u.getMembershipNo(); String access = jwt.generateAccessToken(no, u.getUserId()); String refresh = jwt.generateRefreshToken(no, deviceId != null ? deviceId : "web-oauth"); // Refresh 저장(VALID) - var rt = new RefreshToken(); + RefreshToken rt = new RefreshToken(); rt.setMembershipNo(no); rt.setDeviceId(deviceId != null ? deviceId : "web-oauth"); rt.setToken(refresh); @@ -98,13 +109,21 @@ public LoginResponse loginAfterSignup(String userId, String deviceId) { refreshRepo.save(rt); return new LoginResponse( - new TokenPair(access, accessExp, refresh, refreshExp), + new TokenPair( + access, + accessExp, + refresh, + refreshExp, + ErrorCode.SUCCESS_REFRESH_TOKEN.getCode(), + ErrorCode.SUCCESS_REFRESH_TOKEN.getMessage() + ), no, u.getUserId(), u.getJoinType() ); } @Transactional public TokenPair rotate(String refreshToken) { + try { if (!jwt.isTokenValid(refreshToken) || !jwt.isRefreshToken(refreshToken)) { throw new BadCredentialsException("invalid_refresh_token"); @@ -113,6 +132,9 @@ public TokenPair rotate(String refreshToken) { throw new BadCredentialsException("invalid_refresh_token"); } + String newAccess = ""; + String newRefresh = ""; + String membershipNo = jwt.getMembershipNo(refreshToken); String deviceId = jwt.getDeviceId(refreshToken); @@ -121,46 +143,58 @@ public TokenPair rotate(String refreshToken) { deviceId = "web-oauth"; } - // 현재 리프레시가 DB에 VALID로 존재하는지 확인 - RefreshToken current = refreshRepo.findByTokenAndStatus( - refreshToken, "VALID" - ).orElseThrow(() -> new InvalidRefreshTokenException("refresh_not_found_or_revoked", "로그인을 진행해주세요.")); - - // 만료 체크 - if (current.getExpiresAt().isBefore(LocalDateTime.now())) { - current.setStatus("REVOKED"); - refreshRepo.save(current); - throw new InvalidRefreshTokenException("refresh_expired", "로그인을 다시 진행해주세요."); - } - - // 같은 멤버/디바이스의 기존 VALID 토큰들 모두 REVOKE (동시 세션 차단용) - var olds = refreshRepo.findByMembershipNoAndDeviceIdAndStatus( - membershipNo, deviceId, "VALID" - ); - for (var o : olds) { - o.setStatus("REVOKED"); - } - refreshRepo.saveAll(olds); + try { - // 새 토큰 발급 - var user = userRepo.findById(membershipNo) - .orElseThrow(() -> new InvalidRefreshTokenException("user_not_found", "회원 정보가 존재하지 않습니다.")); + // 현재 리프레시가 DB에 VALID로 존재하는지 확인 + RefreshToken current = refreshRepo.findByTokenAndStatus(refreshToken, "VALID") + .orElseThrow(() -> new InvalidRefreshTokenException(ErrorCode.REQUIRED_LOGIN)); - String newAccess = jwt.generateAccessToken(membershipNo, user.getUserId()); - String newRefresh = jwt.generateRefreshToken(membershipNo, deviceId); + // 만료 체크 + if (current.getExpiresAt().isBefore(LocalDateTime.now())) { + current.setStatus("REVOKED"); + refreshRepo.save(current); + throw new InvalidRefreshTokenException(ErrorCode.REQUIRED_RE_LOGIN); + } - RefreshToken next = new RefreshToken(); - next.setMembershipNo(membershipNo); - next.setDeviceId(deviceId); - next.setToken(newRefresh); - next.setStatus("VALID"); - next.setCreatedAt(LocalDateTime.now()); - next.setExpiresAt(LocalDateTime.now().plusSeconds(jwt.getRefreshExpSeconds())); - refreshRepo.save(next); + // 같은 멤버/디바이스의 기존 VALID 토큰들 모두 REVOKE (동시 세션 차단용) + List olds = refreshRepo.findByMembershipNoAndDeviceIdAndStatus(membershipNo, deviceId, "VALID"); + for (RefreshToken o : olds) { + o.setStatus("REVOKED"); + } + refreshRepo.saveAll(olds); + + // 새 토큰 발급 + UserMembership user = userRepo.findById(membershipNo) + .orElseThrow(() -> new InvalidRefreshTokenException(ErrorCode.NOT_FOUND_USER_INFO)); + + newAccess = jwt.generateAccessToken(membershipNo, user.getUserId()); + newRefresh = jwt.generateRefreshToken(membershipNo, deviceId); + + RefreshToken next = new RefreshToken(); + next.setMembershipNo(membershipNo); + next.setDeviceId(deviceId); + next.setToken(newRefresh); + next.setStatus("VALID"); + next.setCreatedAt(LocalDateTime.now()); + next.setExpiresAt(LocalDateTime.now().plusSeconds(jwt.getRefreshExpSeconds())); + refreshRepo.save(next); + + } catch (InvalidRefreshTokenException ex) { + return new TokenPair( + "", + 0, + "", + 0, + ex.getCode(), + ex.getMessage() + ); + } return new TokenPair( newAccess, jwt.getAccessExpSeconds(), - newRefresh, jwt.getRefreshExpSeconds() + newRefresh, jwt.getRefreshExpSeconds(), + ErrorCode.SUCCESS_REFRESH_TOKEN.getCode(), + ErrorCode.SUCCESS_REFRESH_TOKEN.getMessage() ); } @@ -175,7 +209,7 @@ public void logout(String refreshToken) { List tokens = refreshRepo .findByMembershipNoAndDeviceIdAndStatus(membershipNo, deviceId, "VALID"); - for (var t : tokens) t.setStatus("REVOKED"); + for (RefreshToken t : tokens) t.setStatus("REVOKED"); if (!tokens.isEmpty()) refreshRepo.saveAll(tokens); } @@ -187,5 +221,41 @@ public void logoutAll(String membershipNo) { for (var t : tokens) t.setStatus("REVOKED"); if (!tokens.isEmpty()) refreshRepo.saveAll(tokens); } + + public Map selectUserInfoByToken(String refreshToken) { + + Map returnMap = new HashMap<>(); + + String resultCode = ""; + String message = ""; + String membershipNo = ""; + + try { + // 1. JWT 토큰 유효성 검증 + if(!jwt.isTokenValid(refreshToken) || !jwt.isRefreshToken(refreshToken)) { + throw new InvalidRefreshTokenException(ErrorCode.UNAVAILABLE_REFRESH_TOKEN); + } + + // 2. membershipNo 구하기 + membershipNo = authMapper.selectUserInfoByToken(refreshToken); + + // 3. membershipNo 조회가 되지 않을 경우 + if(membershipNo == null || membershipNo.isBlank()) { + throw new InvalidRefreshTokenException(ErrorCode.NOT_FOUND_AVAILABLE_REFRESH_TOKEN); + } + + resultCode = "200"; + message = "성공"; + returnMap.put("membershipNo", membershipNo); + + } catch (InvalidRefreshTokenException e) { + resultCode = e.getCode(); + message = e.getMessage(); + } finally { + returnMap.put("resultCode", resultCode); + returnMap.put("message", message); + } + return returnMap; + } } diff --git a/src/main/java/com/barogagi/member/login/service/LoginService.java b/src/main/java/com/barogagi/member/login/service/LoginService.java index 29916a7..24fc04b 100644 --- a/src/main/java/com/barogagi/member/login/service/LoginService.java +++ b/src/main/java/com/barogagi/member/login/service/LoginService.java @@ -1,23 +1,207 @@ package com.barogagi.member.login.service; -import com.barogagi.member.login.dto.LoginDTO; -import com.barogagi.member.login.dto.LoginVO; -import com.barogagi.member.login.dto.SearchUserIdDTO; -import com.barogagi.member.login.dto.UserIdDTO; +import com.barogagi.config.PasswordConfig; +import com.barogagi.member.info.dto.Member; +import com.barogagi.member.info.service.MemberService; +import com.barogagi.member.login.dto.*; +import com.barogagi.member.login.exception.LoginException; import com.barogagi.member.login.mapper.LoginMapper; +import com.barogagi.response.ApiResponse; +import com.barogagi.util.EncryptUtil; +import com.barogagi.util.InputValidate; +import com.barogagi.util.Validator; +import com.barogagi.util.exception.ErrorCode; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service public class LoginService { private final LoginMapper loginMapper; + private final Validator validator; + private final InputValidate inputValidate; + private final EncryptUtil encryptUtil; + private final MemberService memberService; + private final PasswordEncoder passwordEncoder; + private final AuthService authService; @Autowired - public LoginService(LoginMapper loginMapper){ + public LoginService( + LoginMapper loginMapper, + Validator validator, + InputValidate inputValidate, + EncryptUtil encryptUtil, + MemberService memberService, + PasswordEncoder passwordEncoder, + AuthService authService + ) + { this.loginMapper = loginMapper; + this.validator = validator; + this.inputValidate = inputValidate; + this.encryptUtil = encryptUtil; + this.memberService = memberService; + this.passwordEncoder = passwordEncoder; + this.authService = authService; + } + + public ApiResponse findUser(SearchUserIdDTO searchUserIdDTO) { + + String resultCode = ""; + String message = ""; + List userIdList = null; + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(searchUserIdDTO.getApiSecretKey())) { + throw new LoginException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(searchUserIdDTO.getTel())) { + throw new LoginException(ErrorCode.EMPTY_DATA); + } + + searchUserIdDTO.setTel(searchUserIdDTO.getTel().replaceAll("[^0-9]", "")); + + searchUserIdDTO.setTel(encryptUtil.encrypt(searchUserIdDTO.getTel())); + List searchIdList = this.myUserIdList(searchUserIdDTO); + + if(searchIdList.isEmpty()) { + resultCode = ErrorCode.NOT_FOUND_ACCOUNT.getCode(); + message = ErrorCode.NOT_FOUND_ACCOUNT.getMessage(); + } else { + resultCode = ErrorCode.FOUND_ACCOUNT.getCode(); + message = ErrorCode.FOUND_ACCOUNT.getMessage(); + userIdList = searchIdList; + } + + return ApiResponse.resultData(userIdList, resultCode, message); + } + + public ApiResponse updatePasswordProcess(LoginDTO loginDTO) { + String resultCode = ""; + String message = ""; + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(loginDTO.getApiSecretKey())) { + throw new LoginException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if (inputValidate.isEmpty(loginDTO.getUserId()) || inputValidate.isEmpty(loginDTO.getPassword())) { + throw new LoginException(ErrorCode.EMPTY_DATA); + } + + // 3. 비밀번호 암호화 + loginDTO.setPassword(passwordEncoder.encode(loginDTO.getPassword())); + + // 4. 비밀번호 update + int updatePassword = this.updatePassword(loginDTO); + if(updatePassword > 0) { + resultCode = ErrorCode.SUCCESS_UPDATE_PASSWORD.getCode(); + message = ErrorCode.SUCCESS_UPDATE_PASSWORD.getMessage(); + } else { + throw new LoginException(ErrorCode.FAIL_UPDATE_PASSWORD); + } + + return ApiResponse.result(resultCode, message); + } + + public ApiResponse login(LoginDTO loginDTO) { + + String resultCode = ""; + String message = ""; + Map dataMap = new HashMap<>(); + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(loginDTO.getApiSecretKey())) { + throw new LoginException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if( + inputValidate.isEmpty(loginDTO.getUserId()) + || inputValidate.isEmpty(loginDTO.getPassword()) + ) + { + throw new LoginException(ErrorCode.EMPTY_DATA); + } + + // 3. 아이디로 회원정보 조회 + Member member = memberService.selectUserMembershipInfo(loginDTO.getUserId()); + if (null == member) { + throw new LoginException(ErrorCode.NOT_FOUND_USER_INFO); + } + + // 4. 비밀번호 일치 여부 + boolean ok = passwordEncoder.matches(loginDTO.getPassword(), member.getPassword()); + if(!ok) { + throw new LoginException(ErrorCode.FAIL_LOGIN); + } + + // 5. ACCESS, REFRESH TOKEN 생성 & REFRESH TOKEN 저장 + LoginResponse loginResponse = authService.loginAfterSignup(member.getUserId(), "web-basic"); + + resultCode = loginResponse.tokens().resultCode(); + message = loginResponse.tokens().message(); + + dataMap = Map.of( + "accessToken", loginResponse.tokens().accessToken(), + "accessTokenExpiresIn", loginResponse.tokens().accessTokenExpiresIn(), + "userId", member.getUserId(), + "membershipNo", loginResponse.membershipNo(), + "refreshToken", loginResponse.tokens().refreshToken(), + "refreshTokenExpiresIn", loginResponse.tokens().refreshTokenExpiresIn() + ); + + return ApiResponse.resultData(dataMap, resultCode, message); + } + + public ApiResponse refreshToken(RefreshTokenRequestDTO refreshTokenRequestDTO) { + + String resultCode = ""; + String message = ""; + Map data = new HashMap<>(); + + // 1. 필수 입력값 확인 + if (inputValidate.isEmpty(refreshTokenRequestDTO.getRefreshToken())) { + throw new LoginException(ErrorCode.EMPTY_DATA); + } + + // 2. ACCESS, REFRESH TOKEN 재생성 + TokenPair pair = authService.rotate(refreshTokenRequestDTO.getRefreshToken()); + + resultCode = pair.resultCode(); + message = pair.message(); + + if(!resultCode.equals("R200")) { + return ApiResponse.error(resultCode, message); + } + + data.put("accessToken", pair.accessToken()); + data.put("accessTokenExpiresIn", pair.accessTokenExpiresIn()); + data.put("refreshToken", pair.refreshToken()); + data.put("refreshTokenExpiresIn", pair.refreshTokenExpiresIn()); + + return ApiResponse.resultData(data, resultCode, message); + } + + public ApiResponse logout(RefreshTokenRequestDTO refreshTokenRequestDTO) { + + // 1. 필수 입력값 확인 + if(inputValidate.isEmpty(refreshTokenRequestDTO.getRefreshToken())) { + throw new LoginException(ErrorCode.EMPTY_DATA); + } + + // 2. 로그아웃 + authService.logout(refreshTokenRequestDTO.getRefreshToken()); // DB REVOKE + + return ApiResponse.result(ErrorCode.SUCCESS_LOGOUT); } public int selectMemberCnt(LoginDTO loginDTO){ @@ -28,13 +212,9 @@ public LoginVO findByUserId(LoginDTO loginDTO) { return loginMapper.findByUserId(loginDTO); } - public List myUserIdList(SearchUserIdDTO searchUserIdDTO){ - return loginMapper.myUserIdList(searchUserIdDTO); - } + public List myUserIdList(SearchUserIdDTO searchUserIdDTO){ return loginMapper.myUserIdList(searchUserIdDTO);} - public int updatePassword(LoginDTO loginDTO){ - return loginMapper.updatePassword(loginDTO); - } + public int updatePassword(LoginDTO loginDTO){return loginMapper.updatePassword(loginDTO);} public LoginVO findMembershipNo(LoginVO vo) { return loginMapper.findMembershipNo(vo); diff --git a/src/main/java/com/barogagi/member/oauth/join/service/CustomOidcUserService.java b/src/main/java/com/barogagi/member/oauth/join/service/CustomOidcUserService.java index 1281136..8bd6cdc 100644 --- a/src/main/java/com/barogagi/member/oauth/join/service/CustomOidcUserService.java +++ b/src/main/java/com/barogagi/member/oauth/join/service/CustomOidcUserService.java @@ -64,7 +64,6 @@ public org.springframework.security.oauth2.core.oidc.user.OidcUser loadUser( joinDTO.setEmail(encryptUtil.encrypt(email)); joinDTO.setNickName(name); joinDTO.setJoinType("GOOGLE"); - joinDTO.setProfileImg(picture); int insertResult = joinService.insertMembershipInfo(joinDTO); diff --git a/src/main/java/com/barogagi/member/oauth/join/service/KakaoOAuth2UserService.java b/src/main/java/com/barogagi/member/oauth/join/service/KakaoOAuth2UserService.java index 51ddbb9..fbabe9e 100644 --- a/src/main/java/com/barogagi/member/oauth/join/service/KakaoOAuth2UserService.java +++ b/src/main/java/com/barogagi/member/oauth/join/service/KakaoOAuth2UserService.java @@ -50,7 +50,6 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic String email = ""; String nickName = ""; - String profileImg = ""; if(null != kakaoAccount) { // 이메일 // 계정에 이메일이 존재하는지 (존재 : ture, 미존재 : false) @@ -69,13 +68,6 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic email = encryptUtil.encrypt((String) kakaoAccount.get("email")); } - // 프로필 - Map profile = (Map) kakaoAccount.get("profile"); - if(null != profile) { - nickName = (String) profile.get("nickname"); - profileImg = (String) profile.get("profile_image_url"); - } - // id에 prefix 추가 id = "provider=kakao" + id; } @@ -98,7 +90,6 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic dto.setUserId(id); dto.setEmail(email); dto.setNickName(nickName); - dto.setProfileImg(profileImg); dto.setJoinType("KAKAO"); int insertResult = joinService.insertMembershipInfo(dto); @@ -114,7 +105,6 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic unified.put("id", id); unified.put("email", email != null ? email : ""); unified.put("name", nickName != null ? nickName : ""); - unified.put("picture", profileImg != null ? profileImg : ""); return new DefaultOAuth2User( List.of(new SimpleGrantedAuthority("ROLE_USER")), diff --git a/src/main/java/com/barogagi/member/oauth/join/service/NaverOAuth2UserService.java b/src/main/java/com/barogagi/member/oauth/join/service/NaverOAuth2UserService.java index 9f52ad0..55e1175 100644 --- a/src/main/java/com/barogagi/member/oauth/join/service/NaverOAuth2UserService.java +++ b/src/main/java/com/barogagi/member/oauth/join/service/NaverOAuth2UserService.java @@ -62,7 +62,6 @@ public OAuth2User loadUser(OAuth2UserRequest req) { String id = str(resp.get("id")); String nickName = str(resp.get("nickname")); - String profileImg = str(resp.get("profile_image")); String gender = str(resp.get("gender")); String email = str(resp.get("email")); String birthday = str(resp.get("birthday")); @@ -87,12 +86,6 @@ public OAuth2User loadUser(OAuth2UserRequest req) { joinDTO.setNickName(nickName); joinDTO.setJoinType("NAVER"); - logger.info("@@ profileImg={}", null != profileImg); - if(null != profileImg) { - // 프로필 사진 - joinDTO.setProfileImg(profileImg); - } - // gender(성별) : M(남성), F(여성), U(미설정) logger.info("@@ gender={}", null != gender); if(null != gender) { diff --git a/src/main/java/com/barogagi/naverblog/client/NaverBlogClient.java b/src/main/java/com/barogagi/naverblog/client/NaverBlogClient.java new file mode 100644 index 0000000..5975a87 --- /dev/null +++ b/src/main/java/com/barogagi/naverblog/client/NaverBlogClient.java @@ -0,0 +1,58 @@ +package com.barogagi.naverblog.client; + +import com.barogagi.naverblog.dto.NaverBlogResDTO; +import com.barogagi.naverblog.dto.NaverBlogSearchResDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; + +@Service +public class NaverBlogClient { + private static final Logger logger = LoggerFactory.getLogger(NaverBlogClient.class); + + @Value("${naver.x-naver-client-id}") + private String xNaverClientId; + + @Value("${naver.x-naver-client-secret}") + private String xNaverClientSecret; + + private final RestTemplate restTemplate = new RestTemplate(); + // https://openapi.naver.com/v1/search/blog.json?query=고궁의아침 강남구&display=5 + + public List searchNaverBlog(String query, int display) { + String url = String.valueOf(UriComponentsBuilder + .fromHttpUrl("https://openapi.naver.com/v1/search/blog.json") + .queryParam("query", query) + .queryParam("display", display) + .build(false)); +// logger.info("#$# url={}", url); + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Naver-Client-Id", xNaverClientId); + headers.set("X-Naver-Client-Secret", xNaverClientSecret); +// logger.info("#$# Request Headers: {}", headers); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + entity, + NaverBlogSearchResDTO.class + ); +// logger.info("#$# response.getBody()={}", response.getBody()); +// logger.info("#$# response.getBody().getItems()={}", response.getBody().getItems()); + List body = response.getBody().getItems(); + + return body; + } + +} diff --git a/src/main/java/com/barogagi/naverblog/dto/NaverBlogResDTO.java b/src/main/java/com/barogagi/naverblog/dto/NaverBlogResDTO.java new file mode 100644 index 0000000..7a45e83 --- /dev/null +++ b/src/main/java/com/barogagi/naverblog/dto/NaverBlogResDTO.java @@ -0,0 +1,33 @@ +package com.barogagi.naverblog.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Data +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "네이버 블로그 검색 결과 DTO") +public class NaverBlogResDTO { + @JsonProperty("title") + private String title; + + @JsonProperty("link") + private String link; + + @JsonProperty("description") + private String description; + + @JsonProperty("bloggername") + private String bloggerName; + + @JsonProperty("bloggerlink") + private String bloggerLink; + + @JsonProperty("postdate") + private String postDate; + +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/naverblog/dto/NaverBlogSearchResDTO.java b/src/main/java/com/barogagi/naverblog/dto/NaverBlogSearchResDTO.java new file mode 100644 index 0000000..6037548 --- /dev/null +++ b/src/main/java/com/barogagi/naverblog/dto/NaverBlogSearchResDTO.java @@ -0,0 +1,30 @@ +package com.barogagi.naverblog.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.util.List; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Schema(description = "네이버 블로그 검색 결과 DTO") +public class NaverBlogSearchResDTO { + @JsonProperty("lastBuildDate") + private String lastBuildDate; + + @JsonProperty("total") + private int total; + + @JsonProperty("start") + private int start; + + @JsonProperty("display") + private int display; + + @JsonProperty("items") + private List items; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/plan/command/entity/Plan.java b/src/main/java/com/barogagi/plan/command/entity/Plan.java index a824330..d5b7065 100644 --- a/src/main/java/com/barogagi/plan/command/entity/Plan.java +++ b/src/main/java/com/barogagi/plan/command/entity/Plan.java @@ -1,9 +1,19 @@ package com.barogagi.plan.command.entity; import com.barogagi.plan.command.ex_entity.PlanUserMembershipInfo; +import com.barogagi.region.command.entity.Place; +import com.barogagi.region.command.entity.PlanRegion; import com.barogagi.schedule.command.entity.Schedule; +import com.barogagi.tag.command.entity.PlanTag; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -27,6 +37,15 @@ public class Plan { @Column(name = "END_TIME") private String endTime; // 종료 시간 + @Column(name = "PLAN_LINK") + public String planLink; // 장소 링크 (이미지 불러오기용) + + @Column(name = "PLAN_DESCRIPTION") + private String planDescription; // 장소 한줄 설명(AI 생성) + + @Column(name = "PLAN_ADDRESS") + private String planAddress; // 장소 주소 + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "SCHEDULE_NUM", nullable = false) private Schedule schedule; // 일정 번호 @@ -38,4 +57,29 @@ public class Plan { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ITEM_NUM", nullable = false) private Item item; // 아이템 번호 + + // PLACE와 1:1 mapping + @OneToOne(mappedBy = "plan", cascade = CascadeType.ALL, orphanRemoval = true) + private Place place; + + @CreationTimestamp + @Column(name = "REG_DATE", updatable = false) + private LocalDateTime regDate; // 등록일 + + @UpdateTimestamp + @Column(name = "UPD_DATE") + private LocalDateTime updDate; // 수정일 (업데이트 시 자동 변경) + + @Column(name = "DEL_YN", nullable = false, columnDefinition = "CHAR(1) DEFAULT 'N'") + private String delYn; // 삭제 여부(Y: 삭제, N: 미삭제) + + @OneToMany(mappedBy = "plan", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List planRegions = new ArrayList<>(); + + @OneToMany(mappedBy = "plan", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List planTags = new ArrayList<>(); + + public void markDeleted() { + this.delYn = "Y"; + } } diff --git a/src/main/java/com/barogagi/plan/command/ex_entity/PlanUserMembershipInfo.java b/src/main/java/com/barogagi/plan/command/ex_entity/PlanUserMembershipInfo.java index 3608cdb..c040379 100644 --- a/src/main/java/com/barogagi/plan/command/ex_entity/PlanUserMembershipInfo.java +++ b/src/main/java/com/barogagi/plan/command/ex_entity/PlanUserMembershipInfo.java @@ -13,7 +13,7 @@ public class PlanUserMembershipInfo { @Id @Column(name = "MEMBERSHIP_NO") - private Integer membershipNo; // 회원번호 + private String membershipNo; // 회원번호 @Column(name = "USER_ID", nullable = false, length = 100) private String userId; // 아이디 diff --git a/src/main/java/com/barogagi/plan/command/repository/ItemRepository.java b/src/main/java/com/barogagi/plan/command/repository/ItemRepository.java new file mode 100644 index 0000000..1e6737f --- /dev/null +++ b/src/main/java/com/barogagi/plan/command/repository/ItemRepository.java @@ -0,0 +1,9 @@ +package com.barogagi.plan.command.repository; + +import com.barogagi.plan.command.entity.Item; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ItemRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/plan/command/repository/PlanRepository.java b/src/main/java/com/barogagi/plan/command/repository/PlanRepository.java index 278a505..afd27df 100644 --- a/src/main/java/com/barogagi/plan/command/repository/PlanRepository.java +++ b/src/main/java/com/barogagi/plan/command/repository/PlanRepository.java @@ -1,9 +1,14 @@ package com.barogagi.plan.command.repository; import com.barogagi.plan.command.entity.Plan; +import com.barogagi.schedule.command.entity.Schedule; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface PlanRepository extends JpaRepository { + + List findBySchedule(Schedule schedule); } diff --git a/src/main/java/com/barogagi/plan/command/repository/PlanTagRepository.java b/src/main/java/com/barogagi/plan/command/repository/PlanTagRepository.java new file mode 100644 index 0000000..a6a6027 --- /dev/null +++ b/src/main/java/com/barogagi/plan/command/repository/PlanTagRepository.java @@ -0,0 +1,7 @@ +package com.barogagi.plan.command.repository; + +import com.barogagi.tag.command.entity.PlanTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlanTagRepository extends JpaRepository { +} diff --git a/src/main/java/com/barogagi/plan/controller/PlaceController.java b/src/main/java/com/barogagi/plan/controller/PlaceController.java new file mode 100644 index 0000000..f589273 --- /dev/null +++ b/src/main/java/com/barogagi/plan/controller/PlaceController.java @@ -0,0 +1,57 @@ +package com.barogagi.plan.controller; + +import com.barogagi.kakaoplace.dto.KakaoPlaceResDTO; +import com.barogagi.plan.query.service.PlaceQueryService; +import com.barogagi.response.ApiResponse; +import com.barogagi.schedule.controller.ScheduleController; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "장소", description = "장소 관련 API") +@RestController +@RequestMapping("/api/v1/place") +public class PlaceController { + + private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class); + + private final PlaceQueryService placeQueryService; + private final String API_SECRET_KEY; + + public PlaceController(Environment environment, + PlaceQueryService placeQueryService) { + this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + this.placeQueryService = placeQueryService; + } + + @Operation(summary = "장소 검색 기능", + description = "사용자가 찾고 싶은 장소를 Kakao API로 검색하는 기능입니다.
" + + "- 일정을 생성하는 API를 호출할 때, 이 API에서 받은 placeName, placeUrl, addressName을 보내주세요.
" + + "- regionNum은 쓰지 않는 필드입니다. (null 값이 전달됨)") + @GetMapping("/keyword-search") + public ApiResponse searchPlace(@Parameter(description = "검색 키워드", example = "스타벅스") + @RequestParam String searchKeyword) { + + logger.info("CALL /place/keyword-search"); + logger.info("[input] searchKeyword={}", searchKeyword); + + List result; + try { + result = placeQueryService.searchPlace(searchKeyword); + + if (result == null) return ApiResponse.error("404", "장소 검색 실패"); + + } catch (Exception e) { + return ApiResponse.error("404", "장소 검색 실패"); + } + + + return ApiResponse.success(result, "장소 검색 성공"); + } +} diff --git a/src/main/java/com/barogagi/plan/dto/PlanDetailResDTO.java b/src/main/java/com/barogagi/plan/dto/PlanDetailResDTO.java deleted file mode 100644 index 1071f25..0000000 --- a/src/main/java/com/barogagi/plan/dto/PlanDetailResDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -/* -package com.barogagi.plan.dto; - -public class PlanDetailResDTO { - private String planNm; // 계획명 - private String startTime; // 시작시간 - private String endTime; // 종료시간 - - private String regionNm; // 지역명 - - // 아이템(구분) - - // Figma에 표시되어 있는 값 - // - 아이콘 이미지 - // - 가게 등 대표 이미지 - - // Figma에 표시되어 있지 않은 값 - // - 태그 -} -*/ diff --git a/src/main/java/com/barogagi/plan/dto/PlanRegistReqDTO.java b/src/main/java/com/barogagi/plan/dto/PlanRegistReqDTO.java index 1b096b8..3f66ab9 100644 --- a/src/main/java/com/barogagi/plan/dto/PlanRegistReqDTO.java +++ b/src/main/java/com/barogagi/plan/dto/PlanRegistReqDTO.java @@ -3,6 +3,7 @@ import com.barogagi.region.dto.RegionRegistReqDTO; import com.barogagi.tag.dto.TagRegistReqDTO; import io.swagger.v3.oas.annotations.media.ArraySchema; +import lombok.Builder; import lombok.Getter; import lombok.ToString; @@ -12,12 +13,10 @@ @Getter @ToString +@Builder(toBuilder = true) @Schema(description = "계획 등록 요청 DTO") public class PlanRegistReqDTO { - @Schema(description = "계획 이름", example = "프랜차이즈카페") - public String planNm; - @Schema(description = "시작 시간", example = "08:00") public String startTime; @@ -30,15 +29,27 @@ public class PlanRegistReqDTO { @Schema(description = "카테고리 번호", example = "1") public int categoryNum; - @Schema(description = "추가 고려사항", example = "한식맛집") - private String comment; // 추가 고려사항 + @Schema(description = "지역 정보 DTO") + public List regionRegistReqDTOList; + + @Schema(description = "계획 태그 목록") + public List planTagRegistReqDTOList; + + @Schema(description = "사용자가 랜덤 카테고리를 선택한 경우", example = "Y") + public String isRandomCategory; + + // 사용자가 직접 세부일정을 추가한 경우에만 필요한 값 + @Schema(description = "사용자가 수동으로 추가한 일정인지 여부(AI 생성 안함)", example = "Y") + public String isUserAdded; + + @Schema(description = "사용자 직접 추가 CASE 1. 카카오 API 장소검색으로 추가한 장소 정보.") + UserAddedPlaceDTO userAddedPlaceDTO; + + @Schema(description = "사용자 직접 추가 CASE 2. 사용자가 직접 입력한 장소", example = "친구집 방문") + public String planNm; + - @Schema(description = "지역 정보 DTO") - public RegionRegistReqDTO regionRegistReqDTO; - @ArraySchema(schema = @Schema(implementation = TagRegistReqDTO.class), - arraySchema = @Schema(description = "태그 리스트", example = "[{\"tagNum\":1,\"tagNm\":\"이색카페\"},{\"tagNum\":2,\"tagNm\":\"맛집투어\"}]")) - public List tagRegistReqDTOList; } diff --git a/src/main/java/com/barogagi/plan/dto/PlanRegistResDTO.java b/src/main/java/com/barogagi/plan/dto/PlanRegistResDTO.java new file mode 100644 index 0000000..d8a35e9 --- /dev/null +++ b/src/main/java/com/barogagi/plan/dto/PlanRegistResDTO.java @@ -0,0 +1,67 @@ +package com.barogagi.plan.dto; + +import com.barogagi.kakaoplace.dto.KakaoPlaceResDTO; +import com.barogagi.plan.enums.PLAN_SOURCE; +import com.barogagi.region.dto.RegionRegistReqDTO; +import com.barogagi.tag.dto.TagRegistReqDTO; +import com.barogagi.tag.dto.TagRegistResDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@Builder(toBuilder = true) +@Schema(description = "계획 등록 응답 DTO") +public class PlanRegistResDTO { + + + @Schema(description = "계획 타입(USER_PLACE/USER_CUSTOM/AI)", example = "AI") + public PLAN_SOURCE planSource; + + @Schema(description = "시작 시간", example = "08:00") + public String startTime; + + @Schema(description = "종료 시간", example = "09:00") + public String endTime; + + @Schema(description = "아이템 번호", example = "1") + public int itemNum; + + @Schema(description = "아이템 명", example = "1") + public String itemNm; + + @Schema(description = "카테고리 번호", example = "1") + public int categoryNum; + + @Schema(description = "카테고리 명", example = "1") + public String categoryNm; + + @Schema(description = "장소 번호") + public Integer planNum; + + @Schema(description = "장소명") + public String planNm; + + @Schema(description = "장소 링크(이미지 불러오기용)") + public String planLink; + + @Schema(description = "장소 한줄 설명(ai 생성)") + public String planDescription; + + @Schema(description = "장소 주소") + public String planAddress; + + @Schema(description = "지역명") + public String regionNm; + + @Schema(description = "지역 번호") + public Integer regionNum; + + @Schema(description = "계획 태그 목록") + public List planTagRegistResDTOList; +} + diff --git a/src/main/java/com/barogagi/plan/dto/UserAddedPlaceDTO.java b/src/main/java/com/barogagi/plan/dto/UserAddedPlaceDTO.java new file mode 100644 index 0000000..3f43843 --- /dev/null +++ b/src/main/java/com/barogagi/plan/dto/UserAddedPlaceDTO.java @@ -0,0 +1,14 @@ +package com.barogagi.plan.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@Schema(description = "계획에 사용자가 수동으로 추가한 장소 정보 DTO") +public class UserAddedPlaceDTO { + private String placeName; + private String placeUrl; + private String addressName; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/plan/enums/PLAN_SOURCE.java b/src/main/java/com/barogagi/plan/enums/PLAN_SOURCE.java new file mode 100644 index 0000000..29aaa6d --- /dev/null +++ b/src/main/java/com/barogagi/plan/enums/PLAN_SOURCE.java @@ -0,0 +1,7 @@ +package com.barogagi.plan.enums; + +public enum PLAN_SOURCE { + USER_PLACE, // 사용자가 장소 선택 + USER_CUSTOM, // 사용자가 직접 입력 + AI // AI 추천으로 생성 +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/plan/query/mapper/CategoryMapper.java b/src/main/java/com/barogagi/plan/query/mapper/CategoryMapper.java new file mode 100644 index 0000000..124ad5f --- /dev/null +++ b/src/main/java/com/barogagi/plan/query/mapper/CategoryMapper.java @@ -0,0 +1,15 @@ +package com.barogagi.plan.query.mapper; + +import com.barogagi.plan.query.vo.PlanDetailVO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CategoryMapper { + + // 카테고리명 조회 + String selectCategoryNmBy(int categoryNum); + + int selectRandomCategoryNum(); +} diff --git a/src/main/java/com/barogagi/plan/query/mapper/ItemMapper.java b/src/main/java/com/barogagi/plan/query/mapper/ItemMapper.java new file mode 100644 index 0000000..a0f4fa2 --- /dev/null +++ b/src/main/java/com/barogagi/plan/query/mapper/ItemMapper.java @@ -0,0 +1,10 @@ +package com.barogagi.plan.query.mapper; + +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ItemMapper { + + // 세부 카테고리(아이템)명 조회 + String selectItemNmBy(int itemNum); +} diff --git a/src/main/java/com/barogagi/plan/query/service/PlaceQueryService.java b/src/main/java/com/barogagi/plan/query/service/PlaceQueryService.java new file mode 100644 index 0000000..441a948 --- /dev/null +++ b/src/main/java/com/barogagi/plan/query/service/PlaceQueryService.java @@ -0,0 +1,25 @@ +package com.barogagi.plan.query.service; + +import com.barogagi.kakaoplace.client.KakaoPlaceClient; +import com.barogagi.kakaoplace.dto.KakaoPlaceResDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PlaceQueryService { + private final KakaoPlaceClient kakaoPlaceClient; + + @Autowired + public PlaceQueryService(KakaoPlaceClient kakaoPlaceClient) { + this.kakaoPlaceClient = kakaoPlaceClient; + } + + public List searchPlace(String searchKeyword) { + + List searchKakaoPlaceList = + kakaoPlaceClient.searchKakaoPlaceByKeyword(searchKeyword); + return searchKakaoPlaceList; + } +} diff --git a/src/main/java/com/barogagi/plan/query/service/PlanQueryService.java b/src/main/java/com/barogagi/plan/query/service/PlanQueryService.java index 7312ee4..d823d3b 100644 --- a/src/main/java/com/barogagi/plan/query/service/PlanQueryService.java +++ b/src/main/java/com/barogagi/plan/query/service/PlanQueryService.java @@ -19,10 +19,7 @@ public PlanQueryService (PlanMapper planMapper) { this.planMapper = planMapper; } - public List getPlanDetail(int scheduleNum) throws Exception{ - logger.info("scheduleNum={}", scheduleNum); - List result = planMapper.selectPlanDetailByScheduleNum(scheduleNum); - logger.info("result={}", result); - return result; + public List getPlanDetail(int scheduleNum){ + return planMapper.selectPlanDetailByScheduleNum(scheduleNum); } } diff --git a/src/main/java/com/barogagi/region/command/entity/Place.java b/src/main/java/com/barogagi/region/command/entity/Place.java new file mode 100644 index 0000000..1f5bc5a --- /dev/null +++ b/src/main/java/com/barogagi/region/command/entity/Place.java @@ -0,0 +1,40 @@ +package com.barogagi.region.command.entity; + +import com.barogagi.plan.command.entity.Plan; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Table(name = "PLACE") +public class Place { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "PLACE_NUM") + private Integer placeNum; + + @Column(name = "REGION_NM", nullable = false) + private String regionNm; + + @Column(name = "ADDRESS") + private String address; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "REGION_NUM") + private Region region; + + @Column(name = "PLAN_LINK") + private String planLink; + + @Column(name = "PLACE_DESCRIPTION") + private String placeDescription; + + // PLAN과 1:1 mapping + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PLAN_NUM", unique = true) + private Plan plan; +} diff --git a/src/main/java/com/barogagi/region/command/entity/PlanRegion.java b/src/main/java/com/barogagi/region/command/entity/PlanRegion.java index 4250b1f..d4e6433 100644 --- a/src/main/java/com/barogagi/region/command/entity/PlanRegion.java +++ b/src/main/java/com/barogagi/region/command/entity/PlanRegion.java @@ -1,8 +1,7 @@ package com.barogagi.region.command.entity; -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; +import com.barogagi.plan.command.entity.Plan; +import jakarta.persistence.*; import lombok.*; @Entity @@ -14,5 +13,15 @@ public class PlanRegion { @EmbeddedId - private PlanRegionNum planRegionNum; // (복합키) 계획 번호, 지역 번호 + private PlanRegionId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("planNum") + @JoinColumn(name = "PLAN_NUM") + private Plan plan; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("regionNum") + @JoinColumn(name = "REGION_NUM") + private Region region; } diff --git a/src/main/java/com/barogagi/region/command/entity/PlanRegionId.java b/src/main/java/com/barogagi/region/command/entity/PlanRegionId.java new file mode 100644 index 0000000..ec048ea --- /dev/null +++ b/src/main/java/com/barogagi/region/command/entity/PlanRegionId.java @@ -0,0 +1,19 @@ +package com.barogagi.region.command.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Embeddable +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PlanRegionId implements Serializable { + private Integer regionNum; + private Integer planNum; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/region/command/entity/PlanRegionNum.java b/src/main/java/com/barogagi/region/command/entity/PlanRegionNum.java deleted file mode 100644 index 30d12ef..0000000 --- a/src/main/java/com/barogagi/region/command/entity/PlanRegionNum.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.barogagi.region.command.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Embeddable -@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 -@AllArgsConstructor -public class PlanRegionNum { - // 계획별 지역 복합키 - - @Column(name="REGION_NUM") - private Integer regionNum; // 지역 번호 - - @Column(name="PLAN_NUM") - private Integer planNum; // 계획 변호 -} diff --git a/src/main/java/com/barogagi/region/command/repository/PlaceRepository.java b/src/main/java/com/barogagi/region/command/repository/PlaceRepository.java new file mode 100644 index 0000000..217127a --- /dev/null +++ b/src/main/java/com/barogagi/region/command/repository/PlaceRepository.java @@ -0,0 +1,7 @@ +package com.barogagi.region.command.repository; + +import com.barogagi.region.command.entity.Place; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlaceRepository extends JpaRepository { +} diff --git a/src/main/java/com/barogagi/region/command/repository/PlanRegionRepository.java b/src/main/java/com/barogagi/region/command/repository/PlanRegionRepository.java new file mode 100644 index 0000000..eddaedd --- /dev/null +++ b/src/main/java/com/barogagi/region/command/repository/PlanRegionRepository.java @@ -0,0 +1,7 @@ +package com.barogagi.region.command.repository; + +import com.barogagi.region.command.entity.PlanRegion; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlanRegionRepository extends JpaRepository { +} diff --git a/src/main/java/com/barogagi/region/controller/RegionController.java b/src/main/java/com/barogagi/region/controller/RegionController.java index 205afb4..d7b6889 100644 --- a/src/main/java/com/barogagi/region/controller/RegionController.java +++ b/src/main/java/com/barogagi/region/controller/RegionController.java @@ -1,9 +1,115 @@ package com.barogagi.region.controller; +import com.barogagi.plan.query.service.PlanQueryService; +import com.barogagi.region.dto.RegionGeoCodeResDTO; +import com.barogagi.region.dto.RegionSearchResDTO; +import com.barogagi.region.query.service.RegionGeoCodeService; +import com.barogagi.region.query.service.RegionQueryService; +import com.barogagi.response.ApiResponse; +import com.barogagi.schedule.command.service.ScheduleCommandService; +import com.barogagi.schedule.controller.ScheduleController; +import com.barogagi.schedule.dto.ScheduleDetailResDTO; +import com.barogagi.schedule.query.service.ScheduleQueryService; +import com.barogagi.util.InputValidate; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +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 java.util.List; + +@Tag(name = "지역", description = "지역 관련 API") @RestController -@RequestMapping("/region") +@RequestMapping("/api/v1/region") public class RegionController { + private static final Logger logger = LoggerFactory.getLogger(RegionController.class); + + private final InputValidate inputValidate; + + private final RegionGeoCodeService regionGeoCodeService; + + private final RegionQueryService regionQueryService; + + private final String API_SECRET_KEY; + + public RegionController(Environment environment, + InputValidate inputValidate, + RegionGeoCodeService regionGeoCodeService, + RegionQueryService regionQueryService) { + this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + this.inputValidate = inputValidate; + this.regionGeoCodeService = regionGeoCodeService; + this.regionQueryService = regionQueryService; + } + @Operation(summary = "주소를 x,y 좌표로 변환하는 기능", description = "주소 번호를 받아서 x,y 좌표로 변환하는 기능입니다") + @GetMapping("/geocode") + public ApiResponse getGeocode(@Parameter(description = "법정동/행정동의 주소 번호", example = "1") @RequestParam Integer regionNum) { + + logger.info("CALL /region/geocode"); + logger.info("[input] regionNum={}", regionNum); + + ApiResponse apiResponse = new ApiResponse(); + String resultCode = ""; + String message = ""; + + try { + + //if(userIdCheckVO.getApiSecretKey().equals(API_SECRET_KEY)){ + if (true) { + + // TODO. 에러 메시지 정의하기 + if (inputValidate.isInvalidInteger(regionNum)) { + resultCode = "101"; + message = "좌표로 변환할 법정동/행정동 번호가 숫자가 아닙니다."; + } else { + + RegionGeoCodeResDTO result = regionGeoCodeService.getGeocode(regionNum); + logger.info("result={}", result.toString()); + + if (result == null) { + resultCode = "300"; + message = "조회할 지역이 존재하지 않습니다."; // TODO. 에러 메시지 정의하기 + + } else { + resultCode = "200"; + message = "주소 좌표 변환 성공"; + apiResponse.setData(result); + } + } + + } else { + resultCode = "100"; + message = "잘못된 접근입니다."; + } + } catch (Exception e) { + resultCode = "400"; + message = "오류가 발생하였습니다."; + throw new RuntimeException(e); + + } finally { + apiResponse.setCode(resultCode); + apiResponse.setMessage(message); + } + return apiResponse; + } + + @Operation(summary = "주소 목록을 검색하는 기능", description = "주소 목록을 검색하는 기능입니다.
" + + "REGION table에 저장된 값 중, level 1부터 4까지 가장 정확도 높은 순으로 지역명 최대 10개를 리턴합니다.
" + + "행정구역 단계(시/도, 시/군/구, 동/면/리)를 조합하여 결과를 반환하며, 중복되는 상위 주소(예: '서울특별시 강남구')는 한 번만 표시됩니다.") + @GetMapping("/search-list") + public ApiResponse searchList(@Parameter(description = "검색할 주소명", example = "강남") @RequestParam String regionQuery) { + + logger.info("CALL /region/searchList"); + logger.info("[input] regionQuery={}", regionQuery); + + List result = regionQueryService.searchList(regionQuery); + return ApiResponse.success(result, "주소 목록 검색 성공"); + + } } diff --git a/src/main/java/com/barogagi/region/dto/RegionGeoCodeResDTO.java b/src/main/java/com/barogagi/region/dto/RegionGeoCodeResDTO.java new file mode 100644 index 0000000..d80660f --- /dev/null +++ b/src/main/java/com/barogagi/region/dto/RegionGeoCodeResDTO.java @@ -0,0 +1,27 @@ +package com.barogagi.region.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder(toBuilder = true) +@ToString +public class RegionGeoCodeResDTO { + @Schema(description = "x 좌표", example = "127.04892851392") + public String x; + + @Schema(description = "y 좌표", example = "37.5091105328378") + public String y; + + public int regionNum; + + public String regionLevel1; + + public String regionLevel2; + + public String regionLevel3; + + public String regionLevel4; +} diff --git a/src/main/java/com/barogagi/region/dto/RegionRegistReqDTO.java b/src/main/java/com/barogagi/region/dto/RegionRegistReqDTO.java index f31eedc..9225d41 100644 --- a/src/main/java/com/barogagi/region/dto/RegionRegistReqDTO.java +++ b/src/main/java/com/barogagi/region/dto/RegionRegistReqDTO.java @@ -1,24 +1,30 @@ package com.barogagi.region.dto; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Getter; import lombok.ToString; @Getter @ToString @Schema(description = "지역 정보 DTO") +@Builder(toBuilder = true) public class RegionRegistReqDTO { - // 카카오 api에서 지역 데이터 어떻게 넘겨주는지 확인 필요 +// @Schema(description = "지역명 대분류", example = "서울특별시") +// public String regionNm1; - @Schema(description = "지역명 대분류", example = "서울특별시") - public String regionNm1; +// @Schema(description = "지역명 소분류", example = "강남구") +// public String regionNm2; - @Schema(description = "지역명 소분류", example = "강남구") - public String regionNm2; +// @Schema(description = "x 좌표", example = "127.04892851392") +// public String x; -// public int regionNum; // 지역 번호 -// public String regionLevel1; // 대분류 -// public String regionLevel2; // 시/군 -// public String regionLevel3; // 구 -// public String regionLevel4; // 동/면/리 +// @Schema(description = "y 좌표", example = "37.5091105328378") +// public String y; + + public int regionNum; // 지역 번호 + public String regionLevel1; // 대분류 + public String regionLevel2; // 시/군 + public String regionLevel3; // 구 + public String regionLevel4; // 동/면/리 } diff --git a/src/main/java/com/barogagi/region/dto/RegionSearchResDTO.java b/src/main/java/com/barogagi/region/dto/RegionSearchResDTO.java new file mode 100644 index 0000000..1e61e5f --- /dev/null +++ b/src/main/java/com/barogagi/region/dto/RegionSearchResDTO.java @@ -0,0 +1,14 @@ +package com.barogagi.region.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@Builder(toBuilder = true) +@ToString +public class RegionSearchResDTO { + public String regionNm; + public Integer regionNum; +} diff --git a/src/main/java/com/barogagi/region/query/mapper/RegionMapper.java b/src/main/java/com/barogagi/region/query/mapper/RegionMapper.java index 0f70e5c..f714a7b 100644 --- a/src/main/java/com/barogagi/region/query/mapper/RegionMapper.java +++ b/src/main/java/com/barogagi/region/query/mapper/RegionMapper.java @@ -10,4 +10,21 @@ public interface RegionMapper { // 계획 상세 조회 - 지역 상세 조회 List selectRegionByPlanNum(int planNum); + + // 지역 번호로 지역명 조회 + RegionDetailVO selectRegionByRegionNum(int regionNum); + + // 키워드로 지역 목록 검색 - level 1부터 가장 정확도 높은 순으로 10개 리턴 + List selectRegionByRegionNm(String regionQuery); + + // 지역 번호로 지역명 조회 (단순 String 반환) +// String selectRegionNameByRegionNum(int regionNum); + + RegionDetailVO selectRegionByLevel4(String level4); + + RegionDetailVO selectRegionByLevel3(String level3); + + RegionDetailVO selectRegionByLevel2(String level2); + + RegionDetailVO selectRegionByLevel1(String level1); } diff --git a/src/main/java/com/barogagi/region/query/service/RegionGeoCodeService.java b/src/main/java/com/barogagi/region/query/service/RegionGeoCodeService.java new file mode 100644 index 0000000..51f255f --- /dev/null +++ b/src/main/java/com/barogagi/region/query/service/RegionGeoCodeService.java @@ -0,0 +1,79 @@ +package com.barogagi.region.query.service; + +import com.barogagi.kakaoplace.client.KakaoGeoCodeClient; +import com.barogagi.kakaoplace.dto.KakaoGeoCodeResDTO; +import com.barogagi.region.dto.RegionGeoCodeResDTO; +import com.barogagi.region.query.mapper.RegionMapper; +import com.barogagi.region.query.vo.RegionDetailVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class RegionGeoCodeService { + private static final Logger logger = LoggerFactory.getLogger(RegionGeoCodeService.class); + + private final KakaoGeoCodeClient kakaoGeoCodeClient; + + private final RegionMapper regionMapper; + + @Autowired + public RegionGeoCodeService(KakaoGeoCodeClient kakaoGeoCodeClient, + RegionMapper regionMapper) { + this.kakaoGeoCodeClient = kakaoGeoCodeClient; + this.regionMapper = regionMapper; + } + + /** + * 주소를 받아서 Kakao API로 좌표(x, y) 변환 + */ + public RegionGeoCodeResDTO getGeocode(Integer regionNum) { + + RegionDetailVO region = regionMapper.selectRegionByRegionNum(regionNum); + + if (region == null) { // todo. 에러 처리 필요 + return null; + } + + // 2. address 문자열 조립 (null/빈 값 무시) + String address = Stream.of( + region.getRegionLevel1(), + region.getRegionLevel2(), + region.getRegionLevel3(), + region.getRegionLevel4() + ) + .filter(Objects::nonNull) + .filter(s -> !s.isBlank()) + .collect(Collectors.joining(" ")); + + logger.info("address = {}", address); + + + // 3. 카카오 API 호출 + List result = kakaoGeoCodeClient.convertKakaoGeoCode(address); + + if (result.isEmpty()) { + return null; + } + + // 4. documents 배열 중 첫 번째 좌표를 DTO에 담아서 리턴 + KakaoGeoCodeResDTO first = result.get(0); + + return RegionGeoCodeResDTO.builder() + .x(first.getX()) + .y(first.getY()) + .regionNum(region.getRegionNum()) + .regionLevel1(region.getRegionLevel1()) + .regionLevel2(region.getRegionLevel2()) + .regionLevel3(region.getRegionLevel3()) + .regionLevel4(region.getRegionLevel4()) + .build(); + } +} + diff --git a/src/main/java/com/barogagi/region/query/service/RegionQueryService.java b/src/main/java/com/barogagi/region/query/service/RegionQueryService.java index 40f4903..ee64460 100644 --- a/src/main/java/com/barogagi/region/query/service/RegionQueryService.java +++ b/src/main/java/com/barogagi/region/query/service/RegionQueryService.java @@ -1,7 +1,151 @@ package com.barogagi.region.query.service; +import com.barogagi.region.dto.RegionSearchResDTO; +import com.barogagi.region.query.mapper.RegionMapper; +import com.barogagi.region.query.vo.RegionDetailVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.*; +import java.util.stream.Collectors; + @Service public class RegionQueryService { + private static final Logger logger = LoggerFactory.getLogger(RegionQueryService.class); + + private final RegionMapper regionMapper; + + @Autowired + public RegionQueryService(RegionMapper regionMapper) { + this.regionMapper = regionMapper; + } + + /** + * 검색어로 지역 정보를 조회하고, + * 각 지역의 행정구역 단계별 주소를 RegionSearchResDTO 리스트로 반환한다. + * + * 예시: + * - CASE 1: 서울특별시 / null / 강남구 / 역삼동 + * → "서울특별시 강남구" + * → "서울특별시 강남구 역삼동" + * + * - CASE 2: 경기도 / 안양시 / 만안구 / 석수동 + * → "경기도 안양시" + * → "경기도 안양시 만안구" + * → "경기도 안양시 만안구 석수동" + * + * - CASE 3: 울산광역시 / 울주군 / null / 온산읍 + * → "울산광역시 울주군" + * → "울산광역시 울주군 온산읍" + * + * @param regionQuery 검색 키워드 + * @return RegionSearchResDTO 리스트 (단계별 주소 포함) + */ + public List searchList(String regionQuery) { + List regionList = regionMapper.selectRegionByRegionNm(regionQuery); + + List result = new ArrayList<>(); + Set seen = new HashSet<>(); // 중복 방지 + + for (RegionDetailVO r : regionList) { + List parts = new ArrayList<>(); + + if (r.getRegionLevel1() != null && !r.getRegionLevel1().isBlank()) { + parts.add(r.getRegionLevel1()); + } + if (r.getRegionLevel2() != null && !r.getRegionLevel2().isBlank()) { + parts.add(r.getRegionLevel2()); + } + if (r.getRegionLevel3() != null && !r.getRegionLevel3().isBlank()) { + parts.add(r.getRegionLevel3()); + } + + // 상위 주소 (레벨1~레벨3까지) → 중복 방지 후 추가 + String upperAddress = String.join(" ", parts); + if (!upperAddress.isBlank() && seen.add(upperAddress)) { + result.add(RegionSearchResDTO.builder() + .regionNum(r.getRegionNum()) + .regionNm(upperAddress) + .build()); + } + + // 레벨4 (동/면/리) + if (r.getRegionLevel4() != null && !r.getRegionLevel4().isBlank()) { + String fullAddress = upperAddress + " " + r.getRegionLevel4(); + if (seen.add(fullAddress)) { + result.add(RegionSearchResDTO.builder() + .regionNum(r.getRegionNum()) + .regionNm(fullAddress) + .build()); + } + } + } + + return result; + } + + public RegionDetailVO getRegionByRegionNum(int regionNum) { + return regionMapper.selectRegionByRegionNum(regionNum); + } + + public RegionDetailVO getRegionNumByAddress(String address) { + + // todo. !!!!!!!!!! 주소가 이상하게 들어감 !!!!!!!! + if (address == null || address.isBlank()) { + throw new IllegalArgumentException("주소가 입력되지 않았습니다."); + } + + String[] parts = address.split(" "); + + List tokens = Arrays.stream(parts) + .filter(t -> !t.matches("\\d+")) + .collect(Collectors.toList()); + + // LEVEL 4 (동/읍/면) + if (tokens.size() >= 3) { + String level4 = tokens.get(2); + RegionDetailVO r4 = regionMapper.selectRegionByLevel4(level4); + logger.info("#$# r4 = {}", r4); + + if (r4 != null) { + return r4; + } + } + logger.info("#$# next?"); + + // LEVEL 3 + if (tokens.size() >= 3) { + String level3 = tokens.get(2); + RegionDetailVO r3 = regionMapper.selectRegionByLevel3(level3); + logger.info("#$# r3 = {}", r3); + + if (r3 != null) { + return r3; + } + } + + // LEVEL 2 + if (tokens.size() >= 2) { + String level2 = tokens.get(1); + RegionDetailVO r2 = regionMapper.selectRegionByLevel2(level2); + if (r2 != null) { + return r2; + } + } + + // LEVEL 1 + if (tokens.size() >= 1) { + String level1 = tokens.get(0); + RegionDetailVO r1 = regionMapper.selectRegionByLevel1(level1); + if (r1 != null) { + return r1; + } + } + + throw new IllegalStateException("입력된 주소로 지역을 찾을 수 없습니다: " + address); + } + } + diff --git a/src/main/java/com/barogagi/response/ApiResponse.java b/src/main/java/com/barogagi/response/ApiResponse.java index 5568d6b..8787b49 100644 --- a/src/main/java/com/barogagi/response/ApiResponse.java +++ b/src/main/java/com/barogagi/response/ApiResponse.java @@ -1,18 +1,57 @@ package com.barogagi.response; +import com.barogagi.util.exception.ErrorCode; import lombok.Getter; import lombok.Setter; +import org.springframework.http.HttpStatus; @Getter @Setter public class ApiResponse { // 결과 코드 - private String resultCode; + private String code; // 결과 메시지 private String message; // 데이터 private T data; + + public static ApiResponse success(T data, String message) { + ApiResponse res = new ApiResponse<>(); + res.code = "SUCCESS"; + res.message = message; + res.data = data; + return res; + } + + public static ApiResponse error(String code, String message) { + ApiResponse res = new ApiResponse<>(); + res.code = code; + res.message = message; + return res; + } + + public static ApiResponse resultData(T data, String code, String message) { + ApiResponse res = new ApiResponse<>(); + res.code = code; + res.message = message; + res.data = data; + return res; + } + + public static ApiResponse result(String code, String message) { + ApiResponse res = new ApiResponse<>(); + res.code = code; + res.message = message; + return res; + } + + public static ApiResponse result(ErrorCode errorCode) { + ApiResponse res = new ApiResponse<>(); + res.code = errorCode.getCode(); + res.message = errorCode.getMessage(); + return res; + } } diff --git a/src/main/java/com/barogagi/schedule/command/entity/Schedule.java b/src/main/java/com/barogagi/schedule/command/entity/Schedule.java index 7d5c65c..d82a790 100644 --- a/src/main/java/com/barogagi/schedule/command/entity/Schedule.java +++ b/src/main/java/com/barogagi/schedule/command/entity/Schedule.java @@ -1,7 +1,14 @@ package com.barogagi.schedule.command.entity; +import com.barogagi.plan.command.entity.Plan; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -17,7 +24,7 @@ public class Schedule { private int scheduleNum; // 일정 번호 (PK) @Column(name = "MEMBERSHIP_NO", nullable = false) - private int membershipNo; // 회원 번호 (FK) + private String membershipNo; // 회원 번호 (FK) @Column(name = "SCHEDULE_NM", nullable = false, length = 100) private String scheduleNm; // 일정명 @@ -30,4 +37,28 @@ public class Schedule { @Column(name = "RADIUS", nullable = false) private int radius; // 추천 반경 (미터 단위) + + @OneToMany(mappedBy = "schedule", cascade = CascadeType.ALL, orphanRemoval = true) + private List plans = new ArrayList<>(); + + @CreationTimestamp + @Column(name = "REG_DATE", updatable = false) + private LocalDateTime regDate; // 등록일 + + @UpdateTimestamp + @Column(name = "UPD_DATE") + private LocalDateTime updDate; // 수정일 (업데이트 시 자동 변경) + + @Column(name = "DEL_YN", nullable = false, columnDefinition = "CHAR(1) DEFAULT 'N'") + private String delYn; // 삭제 여부(Y: 삭제, N: 미삭제) + + public void markDeleted() { + this.delYn = "Y"; + } + + public void updateBasicInfo(String scheduleNm, String startDate, String endDate) { + this.scheduleNm = scheduleNm; + this.startDate = startDate; + this.endDate = endDate; + } } \ No newline at end of file diff --git a/src/main/java/com/barogagi/schedule/command/repository/ScheduleRepository.java b/src/main/java/com/barogagi/schedule/command/repository/ScheduleRepository.java index 5ea08be..0238676 100644 --- a/src/main/java/com/barogagi/schedule/command/repository/ScheduleRepository.java +++ b/src/main/java/com/barogagi/schedule/command/repository/ScheduleRepository.java @@ -4,6 +4,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository -public interface ScheduleRepository extends JpaRepository{ +public interface ScheduleRepository extends JpaRepository{ + Optional findByScheduleNumAndMembershipNo(Integer scheduleNum, String membershipNo); } \ No newline at end of file diff --git a/src/main/java/com/barogagi/schedule/command/service/ScheduleCommandService.java b/src/main/java/com/barogagi/schedule/command/service/ScheduleCommandService.java index 77b867c..4f10536 100644 --- a/src/main/java/com/barogagi/schedule/command/service/ScheduleCommandService.java +++ b/src/main/java/com/barogagi/schedule/command/service/ScheduleCommandService.java @@ -1,7 +1,681 @@ package com.barogagi.schedule.command.service; +import com.barogagi.ai.client.AIClient; +import com.barogagi.ai.dto.AIReqDTO; +import com.barogagi.ai.dto.AIReqWrapper; +import com.barogagi.ai.dto.AIResDTO; +import com.barogagi.kakaoplace.client.KakaoPlaceClient; +import com.barogagi.kakaoplace.dto.KakaoPlaceResDTO; +import com.barogagi.naverblog.client.NaverBlogClient; +import com.barogagi.naverblog.dto.NaverBlogResDTO; +import com.barogagi.plan.command.entity.Item; +import com.barogagi.plan.command.entity.Plan; +import com.barogagi.plan.command.ex_entity.PlanUserMembershipInfo; +import com.barogagi.plan.command.repository.ItemRepository; +import com.barogagi.plan.command.repository.PlanRepository; +import com.barogagi.plan.command.repository.PlanTagRepository; +import com.barogagi.plan.dto.PlanRegistReqDTO; +import com.barogagi.plan.dto.PlanRegistResDTO; +import com.barogagi.plan.dto.UserAddedPlaceDTO; +import com.barogagi.plan.enums.PLAN_SOURCE; +import com.barogagi.plan.query.mapper.CategoryMapper; +import com.barogagi.plan.query.mapper.ItemMapper; +import com.barogagi.region.command.entity.Place; +import com.barogagi.region.command.entity.PlanRegion; +import com.barogagi.region.command.entity.PlanRegionId; +import com.barogagi.region.command.entity.Region; +import com.barogagi.region.command.repository.PlaceRepository; +import com.barogagi.region.command.repository.PlanRegionRepository; +import com.barogagi.region.command.repository.RegionRepository; +import com.barogagi.region.dto.RegionGeoCodeResDTO; +import com.barogagi.region.dto.RegionRegistReqDTO; +import com.barogagi.region.query.service.RegionGeoCodeService; +import com.barogagi.region.query.service.RegionQueryService; +import com.barogagi.region.query.vo.RegionDetailVO; +import com.barogagi.schedule.command.entity.Schedule; +import com.barogagi.schedule.command.repository.ScheduleRepository; +import com.barogagi.schedule.dto.ScheduleRegistReqDTO; +import com.barogagi.schedule.dto.ScheduleRegistResDTO; +import com.barogagi.tag.command.entity.*; +import com.barogagi.tag.command.repository.ScheduleTagRepository; +import com.barogagi.tag.command.repository.TagRepository; +import com.barogagi.tag.dto.TagRegistReqDTO; +import com.barogagi.tag.dto.TagRegistResDTO; +import com.barogagi.tag.query.service.TagQueryService; +import com.barogagi.util.exception.BasicException; +import com.barogagi.util.exception.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.barogagi.util.HtmlUtils.stripHtml; @Service public class ScheduleCommandService { + private static final Logger logger = LoggerFactory.getLogger(ScheduleCommandService.class); + + private final CategoryMapper categoryMapper; + private final ItemMapper itemMapper; + private final KakaoPlaceClient kakaoPlaceClient; + private final NaverBlogClient naverBlogClient; + private final AIClient aiClient; + + private final TagQueryService tagQueryService; + private final RegionGeoCodeService regionGeoCodeService; + + private final ScheduleRepository scheduleRepository; + private final ScheduleTagRepository scheduleTagRepository; + private final TagRepository tagRepository; + private final ItemRepository itemRepository; + private final PlanRepository planRepository; + private final PlanTagRepository planTagRepository; + private final RegionRepository regionRepository; + private final PlanRegionRepository planRegionRepository; + private final PlaceRepository placeRepository; + private final RegionQueryService regionQueryService ; + + + @Value("${kakao.radius}") + private int radius; + + @Value("${naver.display}") + private int naverBlogDisplay; + + @Autowired + public ScheduleCommandService(CategoryMapper categoryMapper, ItemMapper itemMapper, + KakaoPlaceClient kakaoPlaceClient, NaverBlogClient naverBlogClient, + AIClient aiClient, TagQueryService tagQueryService, RegionGeoCodeService regionGeoCodeService, + ScheduleRepository scheduleRepository, ScheduleTagRepository scheduleTagRepository, + TagRepository tagRepository, ItemRepository itemRepository, + PlanRepository planRepository, PlanTagRepository planTagRepository, + RegionRepository regionRepository, PlanRegionRepository planRegionRepository, + PlaceRepository placeRepository, RegionQueryService regionQueryService) { + this.itemMapper = itemMapper; + this.categoryMapper = categoryMapper; + this.kakaoPlaceClient = kakaoPlaceClient; + this.naverBlogClient = naverBlogClient; + this.aiClient = aiClient; + this.tagQueryService = tagQueryService; + this.regionGeoCodeService = regionGeoCodeService; + this.scheduleRepository = scheduleRepository; + this.scheduleTagRepository = scheduleTagRepository; + this.tagRepository = tagRepository; + this.itemRepository = itemRepository; + this.planRepository = planRepository; + this.planTagRepository = planTagRepository; + this.regionRepository = regionRepository; + this.planRegionRepository = planRegionRepository; + this.placeRepository = placeRepository; + this.regionQueryService = regionQueryService; + } + + + + public ScheduleRegistResDTO createSchedule(ScheduleRegistReqDTO scheduleRegistReqDTO) { + + List planResList = new ArrayList<>(); + + // 스케줄 공통 정보 + String scheduleNm = scheduleRegistReqDTO.getScheduleNm(); + String startDate = scheduleRegistReqDTO.getStartDate(); + String endDate = scheduleRegistReqDTO.getEndDate(); + + + scheduleRegistReqDTO.getPlanRegistReqDTOList().forEach(plan -> { + if (plan.getPlanTagRegistReqDTOList() != null) { + plan.getPlanTagRegistReqDTOList().forEach(tag -> { + logger.info("tagNum={}, tagNm={}", tag.getTagNum(), tag.getTagNm()); + }); + } else { + logger.info("태그 없음"); + } + }); + + for (PlanRegistReqDTO plan : scheduleRegistReqDTO.getPlanRegistReqDTOList()) { + + if (plan.getIsUserAdded().equals("Y")) { + // ➜ A. 사용자가 직접 입력한 플랜 + logger.info("A. 사용자가 직접 입력한 플랜 plan={}", plan); + PlanRegistResDTO planRes = handleUserPlan(plan); + planResList.add(planRes); + + } else { + // ➜ B. AI가 추천해줘야 하는 플랜 + logger.info("B. AI가 추천해줘야 하는 플랜 plan={}", plan); + PlanRegistResDTO planRes = handleAIPlan(scheduleRegistReqDTO, plan); + planResList.add(planRes); + } + } + + // ---------- 6) ScheduleRegistResDTO 묶어서 리턴 ---------- + return ScheduleRegistResDTO.builder() + .scheduleNm(scheduleNm) + .startDate(startDate) + .endDate(endDate) + .planRegistResDTOList(planResList) + .build(); + } + + private PlanRegistResDTO handleAIPlan(ScheduleRegistReqDTO scheduleRegistReqDTO, PlanRegistReqDTO plan) { + // ---------- 1) 지역 번호로 x, y 좌표 검색 & Kakao 후보장소 수집(평탄화) ---------- + if (plan.getRegionRegistReqDTOList() == null || plan.getRegionRegistReqDTOList().isEmpty()) { + logger.info("skip: plan has no regions. plan={}", plan); + return null; + } + + int limitPlace = calLimitPlace(plan.getRegionRegistReqDTOList().size()); + + List> allKakaoPlaceResults = new ArrayList<>(); + + // 랜덤 카테고리를 선택한 경우, 카테고리 번호를 랜덤으로 선택 + if (plan.getIsRandomCategory().equals("Y")) { + plan = plan.toBuilder() + .categoryNum(categoryMapper.selectRandomCategoryNum()) + .build(); + logger.info("getCategoryNum={}", plan.getCategoryNum()); + } + + String categoryNm = categoryMapper.selectCategoryNmBy(plan.getCategoryNum()); + String queryString = categoryNm; // 검색어 + + String itemNm = itemMapper.selectItemNmBy(plan.getItemNum()); // todo. itemNm을 검색어로 쓸지 고려하기 + + for (RegionRegistReqDTO region : plan.getRegionRegistReqDTOList()) { + // regionNum으로 좌표 가져오기 + RegionGeoCodeResDTO geo = regionGeoCodeService.getGeocode(region.getRegionNum()); + if (geo == null) { + logger.warn("regionNum={} not found in DB, skip.", region.getRegionNum()); + continue; + } + + RegionRegistReqDTO updatedRegion = region.toBuilder() + .regionLevel1(geo.getRegionLevel1()) + .regionLevel2(geo.getRegionLevel2()) + .regionLevel3(geo.getRegionLevel3()) + .regionLevel4(geo.getRegionLevel4()) + .build(); + + // 리스트 교체 + int idx = plan.getRegionRegistReqDTOList().indexOf(region); + plan.getRegionRegistReqDTOList().set(idx, updatedRegion); + + // 지역명 결정 (레벨2/3 우선순위 적용) + String regionName = null; + if (updatedRegion.getRegionLevel3() != null && !updatedRegion.getRegionLevel3().isEmpty()) { + regionName = updatedRegion.getRegionLevel3(); + } else if (updatedRegion.getRegionLevel2() != null && !updatedRegion.getRegionLevel2().isEmpty()) { + regionName = updatedRegion.getRegionLevel2(); + } + + List oneRegionPlaces = + kakaoPlaceClient.searchKakaoPlace(queryString, geo.getX(), geo.getY(), radius, limitPlace); + allKakaoPlaceResults.add(oneRegionPlaces); + + // 각 장소에 regionNum 세팅 + if (oneRegionPlaces != null) { + oneRegionPlaces.forEach(k -> k.setRegionNum(region.getRegionNum())); + } + + logger.info("resolved regionName={} for regionNum={}", regionName, updatedRegion.getRegionNum()); + + } + + logger.info("allKakaoPlaceResults: {}", allKakaoPlaceResults); + + + // Kakao 평탄화(이 순서를 기준으로 이후 Naver/AI도 동일하게 맞춤) + List flatKakao = allKakaoPlaceResults.stream() + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toList()); + + if (flatKakao.isEmpty()) { + logger.info("no kakao results. plan={}", plan); + return null; + } + + // ---------- 2) Naver 블로그로 title/description 만들기 ---------- + List allBlogsFlat = new ArrayList<>(); + for (KakaoPlaceResDTO k : flatKakao) { + String query = k.getPlaceName() + " " + k.getRoadAddressName(); + List blogs = naverBlogClient.searchNaverBlog(query, naverBlogDisplay); + if (blogs != null) { + allBlogsFlat.addAll(blogs); + } + } + logger.info("allNaverBlogResults.size={}", allBlogsFlat.size()); + + // AI placeList: Kakao 후보 1:1이 가장 안전하지만 현재는 blog기반으로 작성 + // 블로그가 없을 때를 대비해서 Kakao 기본 설명을 fallback으로 변경 + List placeList = new ArrayList<>(); + for (int i = 0; i < flatKakao.size(); i++) { + KakaoPlaceResDTO k = flatKakao.get(i); + // 대응되는 블로그가 없다면 간단한 설명을 생성(fallback) + String title = k.getPlaceName(); + String desc = Optional.ofNullable(k.getCategoryGroupName()).orElse("카테고리 정보 없음") + + " · " + Optional.ofNullable(k.getRoadAddressName()).orElse(k.getAddressName()); + + // 블로그 결과가 있다면 맨 앞 하나만 사용(원한다면 점수화/요약 로직 확장) + if (i < allBlogsFlat.size()) { + NaverBlogResDTO b = allBlogsFlat.get(i); + title = stripHtml(b.getTitle()); + desc = stripHtml(b.getDescription()); + } + + placeList.add(AIReqDTO.builder() + .title(title) + .description(desc) + .build()); + } + + // ---------- 3) AI 호출 ---------- + // todo. 일정 전체에 대한 태그(schedulePlanTagRegistReqDTOList)도 참고하도록 수정해야 함 + List tagNums = Optional.ofNullable(plan.getPlanTagRegistReqDTOList()) + .orElseGet(List::of) + .stream() + .map(TagRegistReqDTO::getTagNum) + .collect(Collectors.toList()); + + List tagNames = tagQueryService.findTagNmByTagNum(tagNums); + + AIReqWrapper aiReqWrapper = AIReqWrapper.builder() + .tags(tagNames) + .comment(Optional.ofNullable(scheduleRegistReqDTO.getComment()).orElse("")) + .placeList(placeList) + .build(); + + AIResDTO aiRes = aiClient.recommandPlace(aiReqWrapper); + + // ---------- 4) AI가 고른 index → Kakao place 선택 ---------- + Integer idx = (aiRes != null) ? aiRes.getRecommandPlaceIndex() : null; + if (idx == null || idx < 0 || idx >= flatKakao.size()) { + idx = 0; // fallback + } + KakaoPlaceResDTO aiChosen = flatKakao.get(idx); + + // ---------- 5) 응답 DTO 생성 ---------- + String regionNm = null; + if (aiChosen.getRegionNum() != null) { + regionNm = regionRepository.findById(aiChosen.getRegionNum()) + .map(region -> { + // 지역명은 보통 3레벨 > 2레벨 순으로 선택 + if (region.getRegionLevel3() != null && !region.getRegionLevel3().isEmpty()) + return region.getRegionLevel3(); + else if (region.getRegionLevel2() != null && !region.getRegionLevel2().isEmpty()) + return region.getRegionLevel2(); + else + return region.getRegionLevel1(); + }) + .orElse(null); + } + + return PlanRegistResDTO.builder() + .planSource(PLAN_SOURCE.AI) + .startTime(plan.getStartTime()) + .endTime(plan.getEndTime()) + .planNm(aiChosen.getPlaceName()) + .planLink(aiChosen.getPlaceUrl()) + .planDescription(aiRes != null ? aiRes.getAiDescription() : null) + .planAddress(Optional.ofNullable(aiChosen.getRoadAddressName()).orElse(aiChosen.getAddressName())) + .regionNm(regionNm) + .regionNum(aiChosen.getRegionNum()) + .categoryNm(categoryNm) + .categoryNum(plan.getCategoryNum()) + .itemNm(itemNm) + .itemNum(plan.getItemNum()) + .planTagRegistResDTOList( + Optional.ofNullable(plan.getPlanTagRegistReqDTOList()) + .orElseGet(List::of) + .stream() + .map(tagReq -> TagRegistResDTO.builder() + .tagNum(tagReq.getTagNum()) + .tagNm(tagReq.getTagNm()) + .build()) + .collect(Collectors.toList()) + ) + // .aiChosen(aiChosen) + .build(); + } + + + private PlanRegistResDTO handleUserPlan(PlanRegistReqDTO plan) { + + // CASE 1: 카카오 장소 ID로 선택한 경우 + if (plan.getUserAddedPlaceDTO() != null) { + logger.info("➜ A-1. 사용자가 직접 입력한 플랜 plan={}", plan); + UserAddedPlaceDTO userAddedPlaceDTO = plan.getUserAddedPlaceDTO(); + + RegionDetailVO regionDetailVO = regionQueryService.getRegionNumByAddress(userAddedPlaceDTO.getAddressName()); + String regionNm = resolveRegionName(regionDetailVO); + + logger.info("사용자가 선택한 장소 userAddedPlaceDTO addressName={}, resolved regionNum={}", userAddedPlaceDTO.getAddressName(), regionDetailVO.getRegionNum()); + + return PlanRegistResDTO.builder() + .planSource(PLAN_SOURCE.USER_PLACE) + .startTime(plan.getStartTime()) + .endTime(plan.getEndTime()) + .planNm(userAddedPlaceDTO.getPlaceName()) + .planLink(userAddedPlaceDTO.getPlaceUrl()) + .planDescription(null) // 사용자가 추가한 일정은 설명 없음 + .planAddress(userAddedPlaceDTO.getAddressName()) + .regionNum(regionDetailVO.getRegionNum()) //todo. check + .regionNm(regionNm) //todo. check + .categoryNum(plan.getCategoryNum()) + .categoryNm(categoryMapper.selectCategoryNmBy(plan.getCategoryNum())) + .itemNum(plan.getItemNum()) + .itemNm(itemMapper.selectItemNmBy(plan.getItemNum())) + .planTagRegistResDTOList(List.of()) // 사용자 일정은 태그 없음 + .build(); + } else { + + // CASE 2: 텍스트로 직접 입력한 경우 + logger.info("➜ A-2. 사용자가 직접 입력한 플랜 plan={}", plan); + + RegionDetailVO region = regionQueryService.getRegionByRegionNum(plan.getRegionRegistReqDTOList().get(0).getRegionNum()); + + return PlanRegistResDTO.builder() + .planSource(PLAN_SOURCE.USER_CUSTOM) + .startTime(plan.getStartTime()) + .endTime(plan.getEndTime()) + .planNm(plan.getPlanNm()) + .planAddress(null) + .planLink(null) + .categoryNum(plan.getCategoryNum()) + .categoryNm(categoryMapper.selectCategoryNmBy(plan.getCategoryNum())) + .itemNum(plan.getItemNum()) + .itemNm(itemMapper.selectItemNmBy(plan.getItemNum())) + .regionNum(plan.getRegionRegistReqDTOList().get(0).getRegionNum()) + .regionNm(region.getRegionLevel3() != null ? region.getRegionLevel3() : region.getRegionLevel2()) + .planTagRegistResDTOList(List.of()) // 사용자 입력은 태그 없음 + .build(); + } + + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Integer saveSchedule(ScheduleRegistResDTO scheduleRegistResDTO, String membershipNo) { + logger.info("START DB SAVE!"); + // 1. Schedule + Schedule schedule = Schedule.builder() + .membershipNo(membershipNo) + .scheduleNm(scheduleRegistResDTO.getScheduleNm()) + .startDate(scheduleRegistResDTO.getStartDate()) + .endDate(scheduleRegistResDTO.getEndDate()) + .radius(radius) + .delYn("N") + .build(); + + scheduleRepository.save(schedule); + logger.info("schedule Save! scheduleNum={}", schedule.getScheduleNum()); + + // 2. Schedule_tag + if (scheduleRegistResDTO.getScheduleTagRegistResDTOList() != null) { + logger.info("schedule save result id={}", schedule.getScheduleNum()); + + for (TagRegistResDTO tagReq : scheduleRegistResDTO.getScheduleTagRegistResDTOList()) { + Tag tag = tagRepository.findById(tagReq.getTagNum()) + .orElseThrow(() -> new BasicException(ErrorCode.TAG_NOT_FOUND)); + + scheduleTagRepository.save( + ScheduleTag.builder() + .id(new ScheduleTagId(tag.getTagNum(), schedule.getScheduleNum())) + .membershipNo(membershipNo) + .schedule(schedule) + .tag(tag) + .build() + ); + + } + } + logger.info("scheduleTag Save! scheduleNum={}", schedule.getScheduleNum()); + + // 3. Plan + Plan_tag + Plan_region + Place + for (int i = 0; i < scheduleRegistResDTO.getPlanRegistResDTOList().size(); i++) { + + PlanRegistResDTO planRes = scheduleRegistResDTO.getPlanRegistResDTOList().get(i); + + Item item = itemRepository.findById(planRes.getItemNum()) + .orElseThrow(() -> new BasicException(ErrorCode.ITEM_NOT_FOUND)); + + PlanUserMembershipInfo user = PlanUserMembershipInfo.builder() + .membershipNo(membershipNo) + .build(); + + // 3-1. Plan + Plan plan = Plan.builder() + .planNum(planRes.getPlanNum()) + .planNm(planRes.getPlanNm()) + .startTime(planRes.getStartTime()) + .endTime(planRes.getEndTime()) + .planLink(planRes.getPlanLink()) + .planDescription(planRes.getPlanDescription()) + .planAddress(planRes.getPlanAddress()) + .schedule(schedule) + .user(user) + .item(item) + .delYn("N") + .build(); + + planRepository.saveAndFlush(plan); + logger.info("Plan save! planNum={}", plan.getPlanNum()); + + + // 3-2. Plan_tag + if (planRes.getPlanTagRegistResDTOList() != null) { + + for (TagRegistResDTO tagRes : planRes.getPlanTagRegistResDTOList()) { + Tag tag = tagRepository.findById(tagRes.getTagNum()) + .orElseThrow(() -> new BasicException(ErrorCode.TAG_NOT_FOUND)); + + PlanTag planTag = PlanTag.builder() + .id(new PlanTagId(tag.getTagNum(), plan.getPlanNum())) + .plan(plan) + .tag(tag) + .build(); + + planTagRepository.save(planTag); + } + } + + // 3-3. Plan_region + if (planRes.getRegionNum() != null) { + Region region = regionRepository.findById(planRes.getRegionNum()) + .orElseThrow(() -> new BasicException(ErrorCode.REGION_NOT_FOUND)); + + PlanRegionId planRegionId = new PlanRegionId(plan.getPlanNum(), region.getRegionNum()); + + PlanRegion planRegion = PlanRegion.builder() + .id(planRegionId) + .region(region) + .plan(plan) + .build(); + + planRegionRepository.save(planRegion); + logger.info("PlanRegion save! regionNum={}, planNum={}", region.getRegionNum(), plan.getPlanNum()); + + } + + // 3-4. Place + if (planRes.getRegionNum() != null) { + Region region = regionRepository.findById(planRes.getRegionNum()) + .orElseThrow(() -> new BasicException(ErrorCode.REGION_NOT_FOUND)); + + Place place = Place.builder() + .region(region) + .regionNm(region.getRegionLevel3() != null ? region.getRegionLevel3() : region.getRegionLevel2()) + .address(planRes.getPlanAddress()) + .planLink(planRes.getPlanLink()) + .placeDescription(planRes.getPlanDescription()) + .plan(plan) + .build(); + + placeRepository.save(place); + logger.info("Place save! planNum={}, regionNum={}", plan.getPlanNum(), planRes.getRegionNum()); + + // 3-5. plan-place 동기화 + plan.toBuilder().place(place).build(); + } + + } + logger.info("END DB SAVE!"); + + return schedule.getScheduleNum(); + + } + + + @Transactional + public boolean deleteSchedule(Integer scheduleNum, String membershipNo) { + Optional optional = scheduleRepository.findByScheduleNumAndMembershipNo(scheduleNum, membershipNo); + if (optional.isPresent()) { + Schedule schedule = optional.get(); + schedule.markDeleted(); // del_yn=Y로 변경 + return true; // 트랜잭션 커밋 시 자동 UPDATE + } + throw new BasicException(ErrorCode.SCHEDULE_NOT_FOUND); + } + + @Transactional + public boolean updateSchedule(ScheduleRegistResDTO dto) { + + // 1) Schedule 조회 + Schedule schedule = scheduleRepository.findById(String.valueOf(dto.getScheduleNum())) + .orElseThrow(() -> new BasicException(ErrorCode.SCHEDULE_NOT_FOUND)); + + // 2) Schedule 기본 정보 업데이트 + schedule.updateBasicInfo( + dto.getScheduleNm(), + dto.getStartDate(), + dto.getEndDate() + ); + + // 3) ScheduleTag 전체 삭제 후 재등록 + scheduleTagRepository.deleteBySchedule(schedule); + + if (dto.getScheduleTagRegistResDTOList() != null) { + for (TagRegistResDTO tagReq : dto.getScheduleTagRegistResDTOList()) { + Tag tag = tagRepository.findById(tagReq.getTagNum()) + .orElseThrow(() -> new BasicException(ErrorCode.TAG_NOT_FOUND)); + + scheduleTagRepository.save( + ScheduleTag.builder() + .id(new ScheduleTagId(tag.getTagNum(), schedule.getScheduleNum())) + .schedule(schedule) + .tag(tag) + .build() + ); + } + } + + // 4) 기존 Plan은 soft delete 처리 + List oldPlans = planRepository.findBySchedule(schedule); + for (Plan p : oldPlans) { + p.markDeleted(); + } + + // 5) 새 Plan + PlanTag + PlanRegion + Place 저장 + if (dto.getPlanRegistResDTOList() != null) { + + for (PlanRegistResDTO planRes : dto.getPlanRegistResDTOList()) { + + Item item = itemRepository.findById(planRes.getItemNum()) + .orElseThrow(() -> new BasicException(ErrorCode.ITEM_NOT_FOUND)); + + PlanUserMembershipInfo user = PlanUserMembershipInfo.builder() + .membershipNo("1") + .build(); + + // 새 Plan 생성 + Plan plan = Plan.builder() + .planNm(planRes.getPlanNm()) + .startTime(planRes.getStartTime()) + .endTime(planRes.getEndTime()) + .planLink(planRes.getPlanLink()) + .planDescription(planRes.getPlanDescription()) + .planAddress(planRes.getPlanAddress()) + .schedule(schedule) + .user(user) + .item(item) + .delYn("N") + .build(); + + planRepository.save(plan); + + // PlanTag 저장 + if (planRes.getPlanTagRegistResDTOList() != null) { + for (TagRegistResDTO tagRes : planRes.getPlanTagRegistResDTOList()) { + Tag tag = tagRepository.findById(tagRes.getTagNum()) + .orElseThrow(() -> new BasicException(ErrorCode.TAG_NOT_FOUND)); + + planTagRepository.save( + new PlanTag(new PlanTagId(tag.getTagNum(), plan.getPlanNum()), plan, tag) + ); + } + } + + // PlanRegion + Place 저장 + if (planRes.getRegionNum() != null) { + Region region = regionRepository.findById(planRes.getRegionNum()) + .orElseThrow(() -> new BasicException(ErrorCode.REGION_NOT_FOUND)); + + // PlanRegion + PlanRegionId regionId = new PlanRegionId(plan.getPlanNum(), region.getRegionNum()); + planRegionRepository.save(new PlanRegion(regionId, plan, region)); + + // Place + Place place = Place.builder() + .region(region) + .regionNm(region.getRegionLevel3() != null ? region.getRegionLevel3() : region.getRegionLevel2()) + .address(planRes.getPlanAddress()) + .planLink(planRes.getPlanLink()) + .placeDescription(planRes.getPlanDescription()) + .plan(plan) + .build(); + + placeRepository.save(place); + } + } + } + + return true; + } + + + // 후보지역 수에 따라 각 지역의 후보장소 수를 리턴 + // 후보장소 수만큼 네이버 블로그 API를 호출해야 하기 때문에 제한 필요 + private int calLimitPlace(int regionCount) { + int perRegionLimit; + if (regionCount == 1) { + perRegionLimit = 5; + } else if (regionCount == 2) { + perRegionLimit = 3; + } else { + perRegionLimit = 2; + } + return perRegionLimit; + } + + private String resolveRegionName(RegionDetailVO region) { + if (region.getRegionLevel3() != null && !region.getRegionLevel3().isEmpty()) { + return region.getRegionLevel3(); + } else if (region.getRegionLevel2() != null && !region.getRegionLevel2().isEmpty()) { + return region.getRegionLevel2(); + } else { + return region.getRegionLevel1(); + } + } + +// private String firstRegionName(List regions) { +// if (regions == null || regions.isEmpty()) return null; +// // RegionRegistReqDTO에 regionName 같은 필드가 있다면 그걸 반환 +// // 예시로 cityName+district 조합 등을 사용 +// return Optional.ofNullable(regions.get(0).getRegionName()).orElse(null); +// } } diff --git a/src/main/java/com/barogagi/schedule/controller/ScheduleController.java b/src/main/java/com/barogagi/schedule/controller/ScheduleController.java index 13b02e5..5c1c108 100644 --- a/src/main/java/com/barogagi/schedule/controller/ScheduleController.java +++ b/src/main/java/com/barogagi/schedule/controller/ScheduleController.java @@ -1,96 +1,501 @@ package com.barogagi.schedule.controller; -//import com.barogagi.member.join.vo.JoinVO; import com.barogagi.plan.query.service.PlanQueryService; import com.barogagi.response.ApiResponse; -import com.barogagi.schedule.dto.ScheduleDetailResDTO; +import com.barogagi.schedule.command.service.ScheduleCommandService; +import com.barogagi.schedule.dto.*; import com.barogagi.schedule.query.service.ScheduleQueryService; import com.barogagi.util.InputValidate; +import com.barogagi.util.MembershipUtil; +import com.barogagi.util.exception.BasicException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; + + @Tag(name = "일정", description = "일정 관련 API") @RestController -@RequestMapping("/schedule") +@RequestMapping("/api/v1/schedule") +@SecurityRequirement(name = "bearerAuth") +@SecurityRequirement(name = "apiKeyAuth") public class ScheduleController { private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class); private final InputValidate inputValidate; private final ScheduleQueryService scheduleQueryService; + private final ScheduleCommandService scheduleCommandService; private final PlanQueryService planQueryService; private final String API_SECRET_KEY; + private final MembershipUtil membershipUtil; public ScheduleController(Environment environment, InputValidate inputValidate, ScheduleQueryService scheduleQueryService, - PlanQueryService planQueryService) { + ScheduleCommandService scheduleCommandService, + PlanQueryService planQueryService, MembershipUtil membershipUtil) { this.API_SECRET_KEY = environment.getProperty("api.secret-key"); this.inputValidate = inputValidate; this.scheduleQueryService = scheduleQueryService; + this.scheduleCommandService = scheduleCommandService; this.planQueryService = planQueryService; + this.membershipUtil = membershipUtil; + } + + @Operation(summary = "내 일정 목록 조회 기능", description = "일정 목록을 조회하는 기능입니다.") + @GetMapping("/list") + public ApiResponse getScheduleList(HttpServletRequest request) { + + logger.info("CALL /api/v1/schedule/list"); + + ScheduleListGroupResDTO result; + try { + + // token으로 membershipNo 조회 + Map resultMap = membershipUtil.membershipNoService(request); + String resultCode = String.valueOf(resultMap.get("resultCode")); + if (!"A200".equals(resultCode)) { + return ApiResponse.error(resultCode, String.valueOf(resultMap.get("message"))); + } + String membershipNo = String.valueOf(resultMap.get("membershipNo")); + + //if(userIdCheckVO.getApiSecretKey().equals(API_SECRET_KEY)){ + result = scheduleQueryService.getScheduleList(membershipNo); + + } catch (Exception e) { + return ApiResponse.error("404", "일정 목록 조회 실패"); + } + + return ApiResponse.success(result, "일정 목록 조회 성공"); } @Operation(summary = "일정 상세 조회 기능", description = "일정을 상세 조회하는 기능입니다.") @GetMapping("/detail") public ApiResponse getScheduleDetail(@Parameter(description = "조회할 일정 번호", example = "1") - @RequestParam Integer scheduleNum) { + @RequestParam Integer scheduleNum, HttpServletRequest request) { - logger.info("CALL /schedule/detail"); - logger.info("[input] SchedulNm={}", scheduleNum); - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; + logger.info("CALL /api/v1/schedule/detail"); + logger.info("[input] scheduleNum={}", scheduleNum); - try { + // token으로 membershipNo 조회 + Map resultMap = membershipUtil.membershipNoService(request); + String resultCode = String.valueOf(resultMap.get("resultCode")); + if (!"A200".equals(resultCode)) { + return ApiResponse.error(resultCode, String.valueOf(resultMap.get("message"))); + } + String membershipNo = String.valueOf(resultMap.get("membershipNo")); + + + ScheduleDetailResDTO result = scheduleQueryService.getScheduleDetail(scheduleNum, membershipNo); + + return ApiResponse.success(result, "일정 조회 성공"); + } + + + @Operation(summary = "일정 생성 기능", + description = "일정을 생성하는 기능입니다.
" + + "- 사용자가 직접 일정을 생성하는 경우는 2가지가 존재합니다.
" + + "     CASE 1. 카카오 장소 검색 API를 사용해서 사용자가 가고 싶은 장소를 선택하는 경우, 카카오 장소 검색 API에서 검색한 placeName, placeUrl, addressName을 보내주세요.
" + + "       주의 1) 사용자가 랜덤 카테고리를 선택한 경우 isRandomCategory=\"Y\"로 전달해 주세요. 이때 categoryNum은 전달하지 않아도 됩니다.
" + + "     CASE 2. 사용자가 세부일정을 직접 텍스트로 입력하는 경우(ex, 친구집 방문), 세부일정명을 planNm 필드에 담아 보내주세요.
" + + "       주의 1) 반드시 isUserAdded=\"Y\"로 전달해 주세요.
" + + "       주의 2) 사용자가 직접 일정을 생성하는 경우 planTagRegistReqDTOList 값을 전달할 필요는 없습니다.
" + + "- 생성된 일정은 '일정 등록'과정을 거쳐야 DB에 저장됩니다.
" + + "- 사용자가 이 API로 생성된 일정을 확인한 후 '일정 생성하기' 버튼을 누르면 '일정 등록' API를 호출해 주세요.") + @PostMapping("/create") + public ApiResponse createSchedule( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "일정 등록 요청", + required = true, + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "일정 등록 요청 예시", + value = "{\n" + + " \"scheduleNm\": \"서울 카페투어\",\n" + + " \"startDate\": \"2025-12-01\",\n" + + " \"endDate\": \"2025-12-01\",\n" + + " \"comment\": \"분위기 좋은 카페 추천해주세요\",\n" + + " \"scheduleTagRegistReqDTOList\": [\n" + + " { \"tagNm\": \"핫플\", \"tagNum\": 5 },\n" + + " { \"tagNm\": \"활동적인\", \"tagNum\": 8 }\n" + + " ],\n" + + " \"planRegistReqDTOList\": [\n" + + " {\n" + + " \"startTime\": \"08:00\",\n" + + " \"endTime\": \"09:00\",\n" + + " \"itemNum\": 10,\n" + + " \"categoryNum\": 2,\n" + + " \"isUserAdded\": \"N\",\n" + + " \"isRandomCategory\": \"Y\",\n" + + " \"regionRegistReqDTOList\": [\n" + + " { \"regionNum\": 1 }\n" + + " ],\n" + + " \"planTagRegistReqDTOList\": [\n" + + " { \"tagNm\": \"디저트맛집\", \"tagNum\": 14 },\n" + + " { \"tagNm\": \"인스타핫플\", \"tagNum\": 15 }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"startTime\": \"14:00\",\n" + + " \"endTime\": \"15:00\",\n" + + " \"itemNum\": 2,\n" + + " \"categoryNum\": 1,\n" + + " \"isUserAdded\": \"Y\",\n" + + " \"isRandomCategory\": \"N\",\n" + + " \"userAddedPlaceDTO\": {\n" + + " \"placeName\": \"카카오프렌즈 코엑스점\",\n" + + " \"placeUrl\": \"http://place.map.kakao.com/26338954\",\n" + + " \"addressName\": \"서울 강남구 삼성동 159\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"startTime\": \"15:30\",\n" + + " \"endTime\": \"19:00\",\n" + + " \"itemNum\": 15,\n" + + " \"categoryNum\": 4,\n" + + " \"isUserAdded\": \"Y\",\n" + + " \"planNm\": \"친구집 방문\",\n" + + " \"regionRegistReqDTOList\": [\n" + + " { \"regionNum\": 1 }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + ) + ) + ) + @RequestBody ScheduleRegistReqDTO scheduleRegistReqDTO + ) { + + logger.info("CALL /api/v1/schedule/create"); + logger.info("[input] scheduleRegistReqDTO={}", scheduleRegistReqDTO); + + ScheduleRegistResDTO result; + try { //if(userIdCheckVO.getApiSecretKey().equals(API_SECRET_KEY)){ - if (true) { - - // TODO. 해당 사용자의 일정이 맞는지도 체크해야 함 - if (inputValidate.isInvalidInteger(scheduleNum)) { - resultCode = "101"; - message = "조회할 일정을 선택해주세요."; - } else { - - ScheduleDetailResDTO result = scheduleQueryService.getScheduleDetail(scheduleNum); - logger.info("result={}", result.toString()); - - if (result == null) { - resultCode = "300"; - message = "조회할 일정이 존재하지 않습니다."; // TODO. 에러 메시지 정의하기 - - } else { - resultCode = "200"; - message = "일정 상세 조회 성공"; - apiResponse.setData(result); - logger.info("#$# result={}", result.toString()); - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; + result = scheduleCommandService.createSchedule(scheduleRegistReqDTO); + + } catch (BasicException e) { + return ApiResponse.error(e.getCode(), e.getMessage()); + } catch (Exception e) { + return ApiResponse.error("404", "일정 생성 실패"); + } + + + return ApiResponse.success(result, "일정 생성 성공"); + } + + + @Operation(summary = "일정 저장 기능", + description = "일정을 DB에 저장하는 기능입니다.
" + + "- '일정 생성하기' 버튼을 눌렀을 때 호출되는 API입니다.
" + + "- '일정 생성' API로 받은 응답 DTO를 그대로 보내주세요.") + @PostMapping("/save") + public ApiResponse saveSchedule( + HttpServletRequest request, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "일정 등록 요청", + required = true, + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "AI 추천 일정만 저장 요청 예시", + value = "{\n" + + " \"scheduleNum\": null,\n" + + " \"scheduleNm\": \"서울 데이트 코스\",\n" + + " \"startDate\": \"2025-07-01\",\n" + + " \"endDate\": \"2025-07-01\",\n" + + " \"scheduleTagRegistResDTOList\": [\n" + + " { \"tagNm\": \"핫플\", \"tagNum\": 5 },\n" + + " { \"tagNm\": \"활동적인\", \"tagNum\": 8 }\n" + + " ],\n" + + " \"planRegistResDTOList\": [\n" + + " {\n" + + " \"planSource\": \"AI\",\n" + + " \"startTime\": \"08:30\",\n" + + " \"endTime\": \"09:00\",\n" + + " \"itemNum\": 10,\n" + + " \"itemNm\": \"프랜차이즈카페\",\n" + + " \"categoryNum\": 2,\n" + + " \"categoryNm\": \"카페\",\n" + + " \"planNm\": \"제비꽃다방\",\n" + + " \"planLink\": \"http://place.map.kakao.com/24944966\",\n" + + " \"planDescription\": \"분위기 좋은 한옥 카페 '더숲 초소책방'은 서울 종로구에 위치해 있으며, 숲속의 아늑함을 느낄 수 있는 넓은 야외 공간과 아름다운 서울 풍경을 감상할 수 있는 2층 테라스가 특징입니다.\",\n" + + " \"planAddress\": \"서울 종로구 창의문로 146\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 14, \"tagNm\": \"디저트맛집\" },\n" + + " { \"tagNum\": 15, \"tagNm\": \"인스타핫플\" }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"planSource\": \"AI\",\n" + + " \"startTime\": \"14:00\",\n" + + " \"endTime\": \"15:00\",\n" + + " \"itemNum\": 2,\n" + + " \"itemNm\": \"한식\",\n" + + " \"categoryNum\": 1,\n" + + " \"categoryNm\": \"식사\",\n" + + " \"planNm\": \"식사\",\n" + + " \"planLink\": \"http://place.map.kakao.com/1581311090\",\n" + + " \"planDescription\": \"분위기 좋은 카페로 뷰가 좋은 곳입니다.\",\n" + + " \"planAddress\": \"서울 중구 무교로 17\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 17, \"tagNm\": \"조용한\" }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"planSource\": \"AI\",\n" + + " \"startTime\": \"15:30\",\n" + + " \"endTime\": \"19:00\",\n" + + " \"itemNum\": 15,\n" + + " \"itemNm\": \"놀이공원\",\n" + + " \"categoryNum\": 4,\n" + + " \"categoryNm\": \"놀거리\",\n" + + " \"planNm\": \"구룡관 혜화본점\",\n" + + " \"planLink\": \"http://place.map.kakao.com/40669117\",\n" + + " \"planDescription\": \"혜화에서 분위기 좋고 저렴한 중식 술집으로는 구룡관 혜화본점이 추천됩니다.\",\n" + + " \"planAddress\": \"서울 종로구 창경궁로 258-5\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 4, \"tagNm\": \"테마파크\" }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + ), + @ExampleObject( + name = "AI 추천 + 사용자 추가 일정 저장 요청 예시", + value = "{\n" + + " \"scheduleNum\": null,\n" + + " \"scheduleNm\": \"3월 여행 일정\",\n" + + " \"startDate\": \"2026-03-11\",\n" + + " \"endDate\": \"2026-03-11\",\n" + + " \"scheduleTagRegistResDTOList\": [\n" + + " { \"tagNm\": \"즐거운\", \"tagNum\": 6 },\n" + + " { \"tagNm\": \"저렴한\", \"tagNum\": 4 }\n" + + " ],\n" + + " \"planRegistResDTOList\": [\n" + + " {\n" + + " \"planSource\": \"AI\",\n" + + " \"startTime\": \"08:30\",\n" + + " \"endTime\": \"09:00\",\n" + + " \"itemNum\": 10,\n" + + " \"itemNm\": \"프랜차이즈카페\",\n" + + " \"categoryNum\": 2,\n" + + " \"categoryNm\": \"카페\",\n" + + " \"planNm\": \"부빙\",\n" + + " \"planLink\": \"http://place.map.kakao.com/20459372\",\n" + + " \"planDescription\": \"'부빙'은 계절마다 변하는 감성 빙수를 판매하는 디저트 카페입니다.\",\n" + + " \"planAddress\": \"서울 종로구 창의문로 136\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 14, \"tagNm\": \"디저트맛집\" },\n" + + " { \"tagNum\": 15, \"tagNm\": \"인스타핫플\" }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"planSource\": \"USER_PLACE\",\n" + + " \"startTime\": \"14:00\",\n" + + " \"endTime\": \"15:00\",\n" + + " \"itemNum\": 2,\n" + + " \"itemNm\": \"한식\",\n" + + " \"categoryNum\": 1,\n" + + " \"categoryNm\": \"식사\",\n" + + " \"planNm\": \"카카오프렌즈 코엑스점\",\n" + + " \"planLink\": \"http://place.map.kakao.com/26338954\",\n" + + " \"planDescription\": null,\n" + + " \"planAddress\": \"서울 강남구 삼성동 159\",\n" + + " \"regionNm\": \"강남구\",\n" + + " \"regionNum\": 9,\n" + + " \"planTagRegistResDTOList\": []\n" + + " },\n" + + " {\n" + + " \"planSource\": \"USER_CUSTOM\",\n" + + " \"startTime\": \"15:30\",\n" + + " \"endTime\": \"19:00\",\n" + + " \"itemNum\": 15,\n" + + " \"itemNm\": \"놀이공원\",\n" + + " \"categoryNum\": 4,\n" + + " \"categoryNm\": \"놀거리\",\n" + + " \"planNm\": \"친구집 방문\",\n" + + " \"planLink\": null,\n" + + " \"planDescription\": null,\n" + + " \"planAddress\": null,\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": []\n" + + " }\n" + + " ]\n" + + "}" + ) + } + ) + ) + @RequestBody ScheduleRegistResDTO scheduleRegistResDTO + ) { + + logger.info("CALL api/v1/schedule/save"); + logger.info("[input] scheduleRegistResDTO={}", scheduleRegistResDTO); + + int result; + try { + // token으로 membershipNo 조회 + Map resultMap = membershipUtil.membershipNoService(request); + String resultCode = String.valueOf(resultMap.get("resultCode")); + if (!"A200".equals(resultCode)) { + return ApiResponse.error(resultCode, String.valueOf(resultMap.get("message"))); } - logger.info("#$# 11 resultCode={}", resultCode); + String membershipNo = String.valueOf(resultMap.get("membershipNo")); + + result = scheduleCommandService.saveSchedule(scheduleRegistResDTO, membershipNo); + } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - logger.info("#$# 22 resultCode={}", resultCode); - logger.info("#$# 22 apiResponse.getResultCode()={}", apiResponse.getResultCode()); + return ApiResponse.error("404", "일정 저장 실패"); } - return apiResponse; + + + return ApiResponse.success(result, "일정 저장 성공"); + } + + @Operation(summary = "일정 수정 기능", + description = "DB에 저장되어 있는 일정을 수정하는 기능입니다.
" + + "- 전체 일정 내 세부 일정을 수정/삭제하는 경우에도 이 API를 호출해주세요.
" + + "- '일정 번호'가 반드시 필요합니다.
" + + "- '일정 조회' API로 받은 응답 DTO를 수정하여 보내주세요.") + @PutMapping("/") + public ApiResponse updateSchedule( + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "일정 등록 요청", + required = true, + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "일정 수정 요청 예시", + value = "{\n" + + " \"scheduleNum\": 1,\n" + + " \"scheduleNm\": \"서울 데이트 코스2\",\n" + + " \"startDate\": \"2025-07-01\",\n" + + " \"endDate\": \"2025-07-01\",\n" + + " \"planRegistResDTOList\": [\n" + + " {\n" + + " \"startTime\": \"08:30\",\n" + + " \"endTime\": \"09:00\",\n" + + " \"itemNum\": 10,\n" + + " \"itemNm\": \"프랜차이즈카페\",\n" + + " \"categoryNum\": 2,\n" + + " \"categoryNm\": \"카페\",\n" + + " \"planNm\": \"제비꽃다방\",\n" + + " \"planLink\": \"http://place.map.kakao.com/24944966\",\n" + + " \"planDescription\": \"분위기 좋은 한옥 카페 '더숲 초소책방'은 서울 종로구에 위치해 있으며, 숲속의 아늑함을 느낄 수 있는 넓은 야외 공간과 아름다운 서울 풍경을 감상할 수 있는 2층 테라스가 특징입니다.\",\n" + + " \"planAddress\": \"서울 종로구 창의문로 146\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 14, \"tagNm\": \"디저트맛집\" },\n" + + " { \"tagNum\": 15, \"tagNm\": \"인스타핫플\" }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"startTime\": \"14:00\",\n" + + " \"endTime\": \"15:00\",\n" + + " \"itemNum\": 2,\n" + + " \"itemNm\": \"한식\",\n" + + " \"categoryNum\": 1,\n" + + " \"categoryNm\": \"식사\",\n" + + " \"planNm\": \"식사\",\n" + + " \"planLink\": \"http://place.map.kakao.com/1581311090\",\n" + + " \"planDescription\": \"분위기 좋은 카페로 뷰가 좋은 곳입니다.\",\n" + + " \"planAddress\": \"서울 중구 무교로 17\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 17, \"tagNm\": \"조용한\" }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"startTime\": \"15:30\",\n" + + " \"endTime\": \"19:00\",\n" + + " \"itemNum\": 15,\n" + + " \"itemNm\": \"놀이공원\",\n" + + " \"categoryNum\": 4,\n" + + " \"categoryNm\": \"놀거리\",\n" + + " \"planNm\": \"구룡관 혜화본점\",\n" + + " \"planLink\": \"http://place.map.kakao.com/40669117\",\n" + + " \"planDescription\": \"혜화에서 분위기 좋고 저렴한 중식 술집으로는 구룡관 혜화본점이 추천됩니다.\",\n" + + " \"planAddress\": \"서울 종로구 창경궁로 258-5\",\n" + + " \"regionNm\": \"종로구\",\n" + + " \"regionNum\": 1,\n" + + " \"planTagRegistResDTOList\": [\n" + + " { \"tagNum\": 4, \"tagNm\": \"테마파크\" }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + ) + ) + ) + @RequestBody ScheduleRegistResDTO scheduleRegistResDTO + ) { + + logger.info("CALL /api/v1/schedule/update"); + logger.info("[input] scheduleRegistResDTO={}", scheduleRegistResDTO); + + boolean result; + try { + //if(userIdCheckVO.getApiSecretKey().equals(API_SECRET_KEY)){ + result = scheduleCommandService.updateSchedule(scheduleRegistResDTO); + + } catch (BasicException e) { + return ApiResponse.error(e.getCode(), e.getMessage()); + } catch (Exception e) { + return ApiResponse.error("404", "일정 저장 실패"); + } + + + return ApiResponse.success(result, "일정 저장 성공"); + } + + @Operation(summary = "일정 전체 삭제 기능", + description = "일정 전체를 DB에서 삭제하는 기능입니다.") + @DeleteMapping("/") + public ApiResponse deleteSchedule(@Parameter(description = "삭제할 일정 번호", example = "1") + @RequestParam Integer scheduleNum, HttpServletRequest request) { + + logger.info("CALL /api/v1/schedule/delete"); + logger.info("[input] scheduleNum={}", scheduleNum); + + // token으로 membershipNo 조회 + Map resultMap = membershipUtil.membershipNoService(request); + String resultCode = String.valueOf(resultMap.get("resultCode")); + if (!"A200".equals(resultCode)) { + return ApiResponse.error(resultCode, String.valueOf(resultMap.get("message"))); + } + String membershipNo = String.valueOf(resultMap.get("membershipNo")); + + // 삭제 처리 + scheduleCommandService.deleteSchedule(scheduleNum, membershipNo); + + return ApiResponse.success(null, "일정 삭제 성공"); } } diff --git a/src/main/java/com/barogagi/schedule/dto/KakaoPlaceReqDTO.java b/src/main/java/com/barogagi/schedule/dto/KakaoPlaceReqDTO.java new file mode 100644 index 0000000..dbb3384 --- /dev/null +++ b/src/main/java/com/barogagi/schedule/dto/KakaoPlaceReqDTO.java @@ -0,0 +1,11 @@ +//package com.barogagi.schedule.dto; +// +//import com.fasterxml.jackson.annotation.JsonProperty; +// +//public class KakaoPlaceReqDTO { +// @JsonProperty("address_name") +// String addressName; // 상세주소 +// +// @JsonProperty("place_name") +// String placeName; // 장소명 +//} diff --git a/src/main/java/com/barogagi/schedule/dto/ScheduleListGroupResDTO.java b/src/main/java/com/barogagi/schedule/dto/ScheduleListGroupResDTO.java new file mode 100644 index 0000000..513f158 --- /dev/null +++ b/src/main/java/com/barogagi/schedule/dto/ScheduleListGroupResDTO.java @@ -0,0 +1,16 @@ +package com.barogagi.schedule.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; +@Getter +@ToString +@Builder(toBuilder = true) +@Schema(description = "과거/미래 일정 목록 조회 DTO") +public class ScheduleListGroupResDTO { + private List pastSchedules; + private List upcomingSchedules; +} diff --git a/src/main/java/com/barogagi/schedule/dto/ScheduleListResDTO.java b/src/main/java/com/barogagi/schedule/dto/ScheduleListResDTO.java new file mode 100644 index 0000000..0f2e736 --- /dev/null +++ b/src/main/java/com/barogagi/schedule/dto/ScheduleListResDTO.java @@ -0,0 +1,22 @@ +package com.barogagi.schedule.dto; + +import com.barogagi.tag.dto.TagRegistResDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@Builder(toBuilder = true) +@Schema(description = "일정 목록 조회 DTO") +public class ScheduleListResDTO { + private int scheduleNum; // 일정 번호 (PK) + private String scheduleNm; // 일정명 + private String startDate; // 시작 날짜 + private String endDate; // 종료 날짜 + + List scheduleTagRegistResDTOList; +} diff --git a/src/main/java/com/barogagi/schedule/dto/ScheduleRegistReqDTO.java b/src/main/java/com/barogagi/schedule/dto/ScheduleRegistReqDTO.java index fdd01f2..5a8ba4c 100644 --- a/src/main/java/com/barogagi/schedule/dto/ScheduleRegistReqDTO.java +++ b/src/main/java/com/barogagi/schedule/dto/ScheduleRegistReqDTO.java @@ -3,6 +3,8 @@ import com.barogagi.plan.dto.PlanRegistReqDTO; import com.barogagi.plan.query.vo.PlanDetailVO; import com.barogagi.region.dto.RegionRegistReqDTO; +import com.barogagi.tag.dto.TagRegistReqDTO; +import com.barogagi.tag.dto.TagRegistResDTO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.ToString; @@ -16,10 +18,10 @@ public class ScheduleRegistReqDTO { private String scheduleNm; // 일정명 private String startDate; // 시작 날짜 private String endDate; // 종료 날짜 - private int radius; // 반경 + private String comment; // 추가 고려사항 - // 지역 리스트 - private List regionRegistReqDTOList; + // 일정 태그 목록 (스케쥴 태그) + public List scheduleTagRegistReqDTOList; // 계획 리스트 private List planRegistReqDTOList; diff --git a/src/main/java/com/barogagi/schedule/dto/ScheduleRegistResDTO.java b/src/main/java/com/barogagi/schedule/dto/ScheduleRegistResDTO.java new file mode 100644 index 0000000..8be5e98 --- /dev/null +++ b/src/main/java/com/barogagi/schedule/dto/ScheduleRegistResDTO.java @@ -0,0 +1,30 @@ +package com.barogagi.schedule.dto; + +import com.barogagi.plan.dto.PlanRegistReqDTO; +import com.barogagi.plan.dto.PlanRegistResDTO; +import com.barogagi.tag.dto.TagRegistReqDTO; +import com.barogagi.tag.dto.TagRegistResDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@Builder(toBuilder = true) +@Schema(description = "일정 등록 응답 DTO") +public class ScheduleRegistResDTO { + private Integer scheduleNum; // 일정 번호 + private String scheduleNm; // 일정명 + private String startDate; // 시작 날짜 + private String endDate; // 종료 날짜 + + // 일정 태그 목록 (스케쥴 태그) + public List scheduleTagRegistResDTOList; + + // 계획 리스트 + private List planRegistResDTOList; + +} diff --git a/src/main/java/com/barogagi/schedule/query/mapper/ScheduleMapper.java b/src/main/java/com/barogagi/schedule/query/mapper/ScheduleMapper.java index 034bf6a..6c8fd84 100644 --- a/src/main/java/com/barogagi/schedule/query/mapper/ScheduleMapper.java +++ b/src/main/java/com/barogagi/schedule/query/mapper/ScheduleMapper.java @@ -1,9 +1,16 @@ package com.barogagi.schedule.query.mapper; +import com.barogagi.schedule.dto.ScheduleListResDTO; import com.barogagi.schedule.query.vo.ScheduleDetailVO; +import com.barogagi.schedule.query.vo.ScheduleListVO; +import com.barogagi.schedule.query.vo.ScheduleMembershipNoVO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + @Mapper public interface ScheduleMapper { - ScheduleDetailVO selectScheduleDetail(int scheduleNum); + ScheduleDetailVO selectScheduleDetail(ScheduleMembershipNoVO scheduleMembershipNoVO); + + List selectScheduleList(String membershipNo); } diff --git a/src/main/java/com/barogagi/schedule/query/service/ScheduleQueryService.java b/src/main/java/com/barogagi/schedule/query/service/ScheduleQueryService.java index 976e0b7..12e2912 100644 --- a/src/main/java/com/barogagi/schedule/query/service/ScheduleQueryService.java +++ b/src/main/java/com/barogagi/schedule/query/service/ScheduleQueryService.java @@ -3,13 +3,20 @@ import com.barogagi.plan.query.service.PlanQueryService; import com.barogagi.plan.query.vo.PlanDetailVO; import com.barogagi.schedule.dto.ScheduleDetailResDTO; +import com.barogagi.schedule.dto.ScheduleListGroupResDTO; +import com.barogagi.schedule.dto.ScheduleListResDTO; import com.barogagi.schedule.query.mapper.ScheduleMapper; import com.barogagi.schedule.query.vo.ScheduleDetailVO; +import com.barogagi.schedule.query.vo.ScheduleListVO; +import com.barogagi.schedule.query.vo.ScheduleMembershipNoVO; +import com.barogagi.util.exception.BasicException; +import com.barogagi.util.exception.ErrorCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.time.LocalDate; import java.util.List; @Service @@ -28,11 +35,50 @@ public ScheduleQueryService (ScheduleMapper scheduleMapper, this.planQueryService = planQueryService; } - public ScheduleDetailResDTO getScheduleDetail(int scheduleNum) throws Exception{ + public ScheduleListGroupResDTO getScheduleList(String membershipNo) { + List scheduleListVOList = scheduleMapper.selectScheduleList(membershipNo); + + // VO -> DTO 변환 + List scheduleListResDTOList = scheduleListVOList.stream() + .map(scheduleListVO -> + ScheduleListResDTO.builder() + .scheduleNum(scheduleListVO.getScheduleNum()) + .scheduleNm(scheduleListVO.getScheduleNm()) + .startDate(scheduleListVO.getStartDate()) + .endDate(scheduleListVO.getEndDate()) + .scheduleTagRegistResDTOList(scheduleListVO.getScheduleTagRegistResDTOList()) + .build() + ).toList(); + + LocalDate today = LocalDate.now(); + + List pastSchedules = scheduleListResDTOList.stream() + .filter(s -> LocalDate.parse(s.getEndDate()).isBefore(today)) + .toList(); + + List upcomingSchedules = scheduleListResDTOList.stream() + .filter(s -> !LocalDate.parse(s.getEndDate()).isBefore(today)) + .toList(); + + return ScheduleListGroupResDTO.builder() + .pastSchedules(pastSchedules) + .upcomingSchedules(upcomingSchedules) + .build(); + } + + + public ScheduleDetailResDTO getScheduleDetail(int scheduleNum, String membershipNo) { + + logger.info("scheduleNum={}, membershipNo={}", scheduleNum, membershipNo); + // 일정 정보 조회 - ScheduleDetailVO scheduleDetailVO = scheduleMapper.selectScheduleDetail(scheduleNum); + ScheduleMembershipNoVO scheduleMembershipNoVO = new ScheduleMembershipNoVO(scheduleNum, membershipNo); + ScheduleDetailVO scheduleDetailVO = scheduleMapper.selectScheduleDetail(scheduleMembershipNoVO); + if(null == scheduleDetailVO) throw new BasicException(ErrorCode.SCHEDULE_NOT_FOUND); + else if(scheduleDetailVO.getDelYn().equals("Y")) throw new BasicException(ErrorCode.SCHEDULE_ALREADY_DELETED); // 계획 정보 조회 (리스트) + logger.info("계획 조회 시작"); List planDetailVOList = planQueryService.getPlanDetail(scheduleNum); // DTO에 정보 저장 diff --git a/src/main/java/com/barogagi/schedule/query/vo/ScheduleDetailVO.java b/src/main/java/com/barogagi/schedule/query/vo/ScheduleDetailVO.java index baf235a..e5df06e 100644 --- a/src/main/java/com/barogagi/schedule/query/vo/ScheduleDetailVO.java +++ b/src/main/java/com/barogagi/schedule/query/vo/ScheduleDetailVO.java @@ -15,4 +15,5 @@ public class ScheduleDetailVO { public String startDate; // 시작 날짜 public String endDate; // 종료 날짜 public int radius; // 반경 + public String delYn; // 삭제 여부 } diff --git a/src/main/java/com/barogagi/schedule/query/vo/ScheduleListVO.java b/src/main/java/com/barogagi/schedule/query/vo/ScheduleListVO.java new file mode 100644 index 0000000..2899893 --- /dev/null +++ b/src/main/java/com/barogagi/schedule/query/vo/ScheduleListVO.java @@ -0,0 +1,18 @@ +package com.barogagi.schedule.query.vo; + +import com.barogagi.tag.dto.TagRegistResDTO; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +public class ScheduleListVO { + public int scheduleNum; // 일정 번호 (PK) + public String scheduleNm; // 일정명 + public String startDate; // 시작 날짜 + public String endDate; // 종료 날짜 + + List scheduleTagRegistResDTOList; +} diff --git a/src/main/java/com/barogagi/schedule/query/vo/ScheduleMembershipNoVO.java b/src/main/java/com/barogagi/schedule/query/vo/ScheduleMembershipNoVO.java new file mode 100644 index 0000000..f6107cf --- /dev/null +++ b/src/main/java/com/barogagi/schedule/query/vo/ScheduleMembershipNoVO.java @@ -0,0 +1,13 @@ +package com.barogagi.schedule.query.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor +public class ScheduleMembershipNoVO { + private Integer scheduleNum; + private String membershipNo; +} diff --git a/src/main/java/com/barogagi/sendSms/service/SendSmsService.java b/src/main/java/com/barogagi/sendSms/service/SendSmsService.java index 721c51d..e8ae681 100644 --- a/src/main/java/com/barogagi/sendSms/service/SendSmsService.java +++ b/src/main/java/com/barogagi/sendSms/service/SendSmsService.java @@ -7,7 +7,6 @@ import net.nurigo.sdk.message.service.DefaultMessageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; @Service @@ -15,16 +14,6 @@ public class SendSmsService { private static final Logger logger = LoggerFactory.getLogger(SendSmsService.class); - private String SEND_TEL = ""; - private String API_KEY = ""; - private String API_SECRET_KEY = ""; - - public SendSmsService(Environment environment) { - this.SEND_TEL = environment.getProperty("send.sms.tel"); - this.API_KEY = environment.getProperty("send.sms.api-key"); - this.API_SECRET_KEY = environment.getProperty("send.sms.api-secret-key"); - } - /** * SMS 발송 * @param sendSmsVO @@ -34,10 +23,14 @@ public boolean sendSms(SendSmsVO sendSmsVO){ boolean result = true; - DefaultMessageService messageService = NurigoApp.INSTANCE.initialize(API_KEY, API_SECRET_KEY, "https://api.solapi.com"); + String sendTel = "01022581349"; + String apiKey = "NCSLFRXVINKTJJRF"; + String apiSecretKey = "FVIG5UM7784HPLYECNCSYF2FVRXVCBWR"; + + DefaultMessageService messageService = NurigoApp.INSTANCE.initialize(apiKey, apiSecretKey, "https://api.solapi.com"); // Message 패키지가 중복될 경우 net.nurigo.sdk.message.model.Message로 치환하여 주세요 Message message = new Message(); - message.setFrom(SEND_TEL); + message.setFrom(sendTel); message.setTo(sendSmsVO.getRecipientTel()); message.setText(sendSmsVO.getMessageContent()); diff --git a/src/main/java/com/barogagi/tag/command/entity/PlanTag.java b/src/main/java/com/barogagi/tag/command/entity/PlanTag.java new file mode 100644 index 0000000..61c9e1a --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/entity/PlanTag.java @@ -0,0 +1,28 @@ +package com.barogagi.tag.command.entity; + +import com.barogagi.plan.command.entity.Plan; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Table(name = "PLAN_TAG") +public class PlanTag { + + @EmbeddedId + private PlanTagId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("planNum") + @JoinColumn(name = "PLAN_NUM") + private Plan plan; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("tagNum") + @JoinColumn(name = "TAG_NUM") + private Tag tag; +} + diff --git a/src/main/java/com/barogagi/tag/command/entity/PlanTagId.java b/src/main/java/com/barogagi/tag/command/entity/PlanTagId.java new file mode 100644 index 0000000..1f5fdf4 --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/entity/PlanTagId.java @@ -0,0 +1,19 @@ +package com.barogagi.tag.command.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Embeddable +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PlanTagId implements Serializable { + private Integer tagNum; + private Integer planNum; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/command/entity/ScheduleTag.java b/src/main/java/com/barogagi/tag/command/entity/ScheduleTag.java new file mode 100644 index 0000000..1b206af --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/entity/ScheduleTag.java @@ -0,0 +1,35 @@ +package com.barogagi.tag.command.entity; + +import com.barogagi.schedule.command.entity.Schedule; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Table(name = "SCHEDULE_TAG") +@EqualsAndHashCode +public class ScheduleTag { + + @EmbeddedId + private ScheduleTagId id; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("scheduleNum") + @JoinColumn(name = "SCHEDULE_NUM") + private Schedule schedule; + + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("tagNum") + @JoinColumn(name = "TAG_NUM") + private Tag tag; + + @Column(name = "MEMBERSHIP_NO", nullable = false) + private String membershipNo; + + +} + + diff --git a/src/main/java/com/barogagi/tag/command/entity/ScheduleTagId.java b/src/main/java/com/barogagi/tag/command/entity/ScheduleTagId.java new file mode 100644 index 0000000..94efa0b --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/entity/ScheduleTagId.java @@ -0,0 +1,19 @@ +package com.barogagi.tag.command.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +@Embeddable +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ScheduleTagId implements Serializable { + private Integer tagNum; + private Integer scheduleNum; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/command/entity/Tag.java b/src/main/java/com/barogagi/tag/command/entity/Tag.java new file mode 100644 index 0000000..72ecd43 --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/entity/Tag.java @@ -0,0 +1,33 @@ +package com.barogagi.tag.command.entity; + + +import com.barogagi.plan.command.entity.Category; +import com.barogagi.tag.enums.TagType; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) // 무분별한 객체 생성 방지 +@AllArgsConstructor +@Builder(toBuilder = true) +@Table(name = "TAG") +@ToString(exclude = "category") +public class Tag { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "TAG_NUM") + private Integer tagNum; + + @Column(name = "TAG_NM", nullable = false, length = 100) + private String tagNm; + + @Enumerated(EnumType.STRING) + @Column(name = "TAG_TYPE", nullable = false, length = 1) + private TagType tagType; // ENUM('P', 'S') + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "CATEGORY_NUM") + private Category category; +} diff --git a/src/main/java/com/barogagi/tag/command/repository/ScheduleTagRepository.java b/src/main/java/com/barogagi/tag/command/repository/ScheduleTagRepository.java new file mode 100644 index 0000000..64f35bc --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/repository/ScheduleTagRepository.java @@ -0,0 +1,14 @@ +package com.barogagi.tag.command.repository; + +import com.barogagi.schedule.command.entity.Schedule; +import com.barogagi.tag.command.entity.ScheduleTag; +import com.barogagi.tag.command.entity.ScheduleTagId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ScheduleTagRepository extends JpaRepository { + + void deleteBySchedule(Schedule schedule); + +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/command/repository/TagRepository.java b/src/main/java/com/barogagi/tag/command/repository/TagRepository.java new file mode 100644 index 0000000..c931a22 --- /dev/null +++ b/src/main/java/com/barogagi/tag/command/repository/TagRepository.java @@ -0,0 +1,12 @@ +package com.barogagi.tag.command.repository; +import com.barogagi.tag.command.entity.Tag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TagRepository extends JpaRepository { +// Optional findById(Integer tagNum); +} + diff --git a/src/main/java/com/barogagi/tag/controller/TagController.java b/src/main/java/com/barogagi/tag/controller/TagController.java new file mode 100644 index 0000000..19cf516 --- /dev/null +++ b/src/main/java/com/barogagi/tag/controller/TagController.java @@ -0,0 +1,54 @@ +package com.barogagi.tag.controller; + +import com.barogagi.response.ApiResponse; +import com.barogagi.schedule.dto.ScheduleRegistReqDTO; +import com.barogagi.tag.dto.TagSearchReqDTO; +import com.barogagi.tag.dto.TagSearchResDTO; +import com.barogagi.tag.query.service.TagQueryService; +import com.barogagi.util.InputValidate; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "태그", description = "태그 관련 API") +@RestController +@RequestMapping("/api/v1/tag") +public class TagController { + private static final Logger logger = LoggerFactory.getLogger(TagController.class); + + private final InputValidate inputValidate; + + private final String API_SECRET_KEY; + + private final TagQueryService tagQueryService; + + public TagController(Environment environment, InputValidate inputValidate, + TagQueryService tagQueryService) { + this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + this.inputValidate = inputValidate; + this.tagQueryService = tagQueryService; + } + + @Operation( + summary = "태그 목록 검색", + description = "태그 목록을 검색하는 기능입니다.
" + + "- 여행 스타일 태그(S): categoryNum을 null로 전달하세요.
" + + "- 상세 일정 태그(P): 해당 일정의 카테고리 번호(categoryNum)를 전달하세요.
" + + "검색 결과는 최대 10개의 태그를 반환합니다." + ) + @PostMapping("/search-list") + public ApiResponse searchList(@RequestBody TagSearchReqDTO tagSearchReqDTO) { + + logger.info("CALL /tag/searchList"); + + List result = tagQueryService.searchList(tagSearchReqDTO); + return ApiResponse.success(result, "태그 목록 검색 성공"); + + } + +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/dto/TagRegistResDTO.java b/src/main/java/com/barogagi/tag/dto/TagRegistResDTO.java new file mode 100644 index 0000000..2671ef6 --- /dev/null +++ b/src/main/java/com/barogagi/tag/dto/TagRegistResDTO.java @@ -0,0 +1,18 @@ +package com.barogagi.tag.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@Builder(toBuilder = true) +@Schema(description = "태그 정보 리스트 DTO") +public class TagRegistResDTO { + @Schema(description = "태그 번호", example = "1") + public int tagNum; + + @Schema(description = "태그 이름", example = "디저트") + public String tagNm; +} diff --git a/src/main/java/com/barogagi/tag/dto/TagSearchReqDTO.java b/src/main/java/com/barogagi/tag/dto/TagSearchReqDTO.java new file mode 100644 index 0000000..4dc33bb --- /dev/null +++ b/src/main/java/com/barogagi/tag/dto/TagSearchReqDTO.java @@ -0,0 +1,14 @@ +package com.barogagi.tag.dto; + +import com.barogagi.tag.enums.TagType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +public class TagSearchReqDTO { + @Schema(description = "태그 타입 (S는 스타일 태그, P는 세부 일정 태그)", example = "P") + public TagType tagType; + + @Schema(description = "카테고리 번호 (스타일 태그인 경우 null)", example = "1") + public Integer categoryNum; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/dto/TagSearchResDTO.java b/src/main/java/com/barogagi/tag/dto/TagSearchResDTO.java new file mode 100644 index 0000000..7c319f9 --- /dev/null +++ b/src/main/java/com/barogagi/tag/dto/TagSearchResDTO.java @@ -0,0 +1,16 @@ +package com.barogagi.tag.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@Schema(description = "태그 검색 결과 DTO") +public class TagSearchResDTO { + @Schema(description = "태그 번호", example = "1") + public int tagNum; + + @Schema(description = "태그 이름", example = "디저트") + public String tagNm; +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/enums/TagType.java b/src/main/java/com/barogagi/tag/enums/TagType.java new file mode 100644 index 0000000..d4659a6 --- /dev/null +++ b/src/main/java/com/barogagi/tag/enums/TagType.java @@ -0,0 +1,31 @@ +package com.barogagi.tag.enums; + +public enum TagType { + S("일정별 태그"), + P("계획별 태그"); + + private final String description; + + TagType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + // DB 저장용 코드 반환 + public String getCode() { + return this.name(); + } + + // 코드로부터 Enum 찾기 + public static TagType fromCode(String code) { + for (TagType type : TagType.values()) { + if (type.name().equalsIgnoreCase(code)) { + return type; + } + } + throw new IllegalArgumentException("Unknown TagType code: " + code); + } +} diff --git a/src/main/java/com/barogagi/tag/query/mapper/TagMapper.java b/src/main/java/com/barogagi/tag/query/mapper/TagMapper.java index 42005f8..00a0c62 100644 --- a/src/main/java/com/barogagi/tag/query/mapper/TagMapper.java +++ b/src/main/java/com/barogagi/tag/query/mapper/TagMapper.java @@ -1,5 +1,7 @@ package com.barogagi.tag.query.mapper; +import com.barogagi.tag.dto.TagSearchReqDTO; +import com.barogagi.tag.dto.TagSearchResDTO; import com.barogagi.tag.query.vo.TagDetailVO; import org.apache.ibatis.annotations.Mapper; @@ -10,4 +12,10 @@ public interface TagMapper { // 계획 상세 조회 - 태그 상세 조회 List selectTagByPlanNum (int planNum); + + // 일정 생성 - 태그 번호로 태그명 조회 + TagDetailVO selectTagByTagNum (int tagNum); + + // 태그 리스트 조회 + List selectTagByTagTypeAndCategoryNum(TagSearchReqDTO tagSearchReqDTO); } diff --git a/src/main/java/com/barogagi/tag/query/service/TagQueryService.java b/src/main/java/com/barogagi/tag/query/service/TagQueryService.java new file mode 100644 index 0000000..bf1e3ee --- /dev/null +++ b/src/main/java/com/barogagi/tag/query/service/TagQueryService.java @@ -0,0 +1,44 @@ +package com.barogagi.tag.query.service; + +import com.barogagi.schedule.command.service.ScheduleCommandService; +import com.barogagi.schedule.dto.ScheduleRegistReqDTO; +import com.barogagi.tag.dto.TagSearchReqDTO; +import com.barogagi.tag.dto.TagSearchResDTO; +import com.barogagi.tag.query.mapper.TagMapper; +import com.barogagi.tag.query.vo.TagDetailVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.stream.Collectors; + +import java.util.List; + +@Service +public class TagQueryService { + private static final Logger logger = LoggerFactory.getLogger(TagQueryService.class); + + private final TagMapper tagMapper; + + @Autowired + public TagQueryService (TagMapper tagMapper) { + this.tagMapper = tagMapper; + } + + // 계획 번호로 연결된 태그 상세 리스트 조회 + public List findTagByPlanNum(int planNum) { + return tagMapper.selectTagByPlanNum(planNum); + } + + // 태그 번호 리스트로 태그명 리스트 조회 + public List findTagNmByTagNum(List tagNums) { + return tagNums.stream() + .map(tagMapper::selectTagByTagNum) + .map(TagDetailVO::getTagNm) + .collect(Collectors.toList()); + } + + public List searchList(TagSearchReqDTO tagSearchReqDTO) { + return tagMapper.selectTagByTagTypeAndCategoryNum(tagSearchReqDTO); + } +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/tag/query/vo/TagDetailVO.java b/src/main/java/com/barogagi/tag/query/vo/TagDetailVO.java index 4748a9b..382fd4c 100644 --- a/src/main/java/com/barogagi/tag/query/vo/TagDetailVO.java +++ b/src/main/java/com/barogagi/tag/query/vo/TagDetailVO.java @@ -1,5 +1,6 @@ package com.barogagi.tag.query.vo; +import com.barogagi.tag.enums.TagType; import lombok.Getter; import lombok.ToString; @@ -8,4 +9,6 @@ public class TagDetailVO { private int tagNum; // 태그 번호 private String tagNm; // 태그명 + private TagType tagType; // 태그 타입 + private int categoryNum; // 카테고리 번호 (일정(스타일) 태그는 null, 계획(plan) 태그는 카테고리 번호) } diff --git a/src/main/java/com/barogagi/tag/query/vo/TagSimpleVO.java b/src/main/java/com/barogagi/tag/query/vo/TagSimpleVO.java new file mode 100644 index 0000000..6ce47ea --- /dev/null +++ b/src/main/java/com/barogagi/tag/query/vo/TagSimpleVO.java @@ -0,0 +1,11 @@ +package com.barogagi.tag.query.vo; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class TagSimpleVO { + private int tagNum; // 태그 번호 + private String tagNm; // 태그명 +} \ No newline at end of file diff --git a/src/main/java/com/barogagi/terms/controller/TermsController.java b/src/main/java/com/barogagi/terms/controller/TermsController.java index 92daee9..2863e82 100644 --- a/src/main/java/com/barogagi/terms/controller/TermsController.java +++ b/src/main/java/com/barogagi/terms/controller/TermsController.java @@ -1,145 +1,45 @@ package com.barogagi.terms.controller; -import com.barogagi.member.login.dto.LoginVO; -import com.barogagi.member.login.service.LoginService; import com.barogagi.response.ApiResponse; +import com.barogagi.terms.dto.*; import com.barogagi.terms.service.TermsService; -import com.barogagi.terms.dto.TermsInputDTO; -import com.barogagi.terms.dto.TermsDTO; -import com.barogagi.terms.dto.TermsOutputDTO; -import com.barogagi.terms.dto.TermsProcessDTO; -import com.barogagi.util.InputValidate; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.util.List; - @Tag(name = "약관", description = "약관 관련 API") @RestController -@RequestMapping("/terms") +@RequestMapping("/api/v1/terms") +@RequiredArgsConstructor public class TermsController { - private static final Logger logger = LoggerFactory.getLogger(TermsController.class); - - private InputValidate inputValidate; - private TermsService termsService; - private LoginService loginService; - - private final String API_SECRET_KEY; - @Autowired - public TermsController(Environment environment, InputValidate inputValidate, - TermsService termsService, LoginService loginService){ - this.inputValidate = inputValidate; - this.termsService = termsService; - this.loginService = loginService; - this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + private final TermsService termsService; + + @Operation(summary = "약관 목록 조회", description = "약관 목록 조회 기능입니다.
회원가입 시 사용할 경우 termsType 값을 JOIN-MEMBERSHIP 값으로 넣어주세요.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "T200", description = "약관 조회에 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "T102", description = "약관이 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @GetMapping + public ApiResponse termsList(@RequestHeader("API-KEY") String apiSecretKey, @RequestParam String termsType){ + return termsService.termsListProcess(apiSecretKey, termsType); } - @Operation(summary = "약관 목록 조회", description = "약관 목록 조회 기능입니다. apiSecretKey와 termsType값만 보내주시면 됩니다.") - @PostMapping("/list") - public ApiResponse termsList(@RequestBody TermsInputDTO termsInputDTO){ - logger.info("CALL /terms/list"); - logger.info("[input] API_SECRET_KEY={}", termsInputDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if(termsInputDTO.getApiSecretKey().equals(API_SECRET_KEY)) { - - if(inputValidate.isEmpty(termsInputDTO.getTermsType())) { - resultCode = "101"; - message = "조회하실 약관의 종류 값이 존재하지 않습니다."; - } else { - List termsList = termsService.selectTermsList(termsInputDTO); - - int termsCnt = termsList.size(); - logger.info("termsCnt={}", termsCnt); - if(termsCnt > 0) { - resultCode = "200"; - message = "약관 조회에 성공하였습니다."; - apiResponse.setData(termsList); - - } else { - resultCode = "102"; - message = "약관이 존재하지 않습니다."; - } - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - - return apiResponse; - } - - @Operation(summary = "약관 동의 여부 저장", description = "약관 동의 여부 저장 기능입니다.") - @PostMapping("/agree/insert") + @Operation(summary = "약관 동의 여부 저장", description = "약관 동의 여부 저장 기능입니다.", + responses = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "T200", description = "약관 저장에 성공하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "A100", description = "API SECRET KEY 불일치"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "C101", description = "정보를 입력해주세요."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "L102", description = "해당 사용자의 정보가 존재하지 않습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "T300", description = "약관 저장에 실패하였습니다."), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "오류가 발생하였습니다.") + }) + @PostMapping("/terms-agreements") public ApiResponse insertTermsAgree(@RequestBody TermsDTO termsDTO) { - logger.info("CALL /agree/insert"); - logger.info("[input] API_SECRET_KEY={}", termsDTO.getApiSecretKey()); - - ApiResponse apiResponse = new ApiResponse(); - String resultCode = ""; - String message = ""; - - try { - if(termsDTO.getApiSecretKey().equals(API_SECRET_KEY)) { - - String userId = termsDTO.getUserId(); - LoginVO lvo = new LoginVO(); - lvo.setUserId(userId); - - LoginVO loginVO = loginService.findMembershipNo(lvo); - if(null != loginVO) { - List termsAgreeList = termsDTO.getTermsAgreeList(); - for(TermsProcessDTO termsProcessDTO : termsAgreeList) { - termsProcessDTO.setMembershipNo(loginVO.getMembershipNo()); - } - String resCode = termsService.insertTermsAgreeList(termsAgreeList); - if(resCode.equals("200")) { - resultCode = "200"; - message = "약관 저장에 성공하였습니다."; - } else { - resultCode = "300"; - message = "약관 저장에 실패하였습니다."; - } - - } else { - resultCode = "101"; - message = "해당 사용자의 정보가 존재하지 않습니다."; - } - - } else { - resultCode = "100"; - message = "잘못된 접근입니다."; - } - - } catch (Exception e) { - resultCode = "400"; - message = "오류가 발생하였습니다."; - throw new RuntimeException(e); - } finally { - apiResponse.setResultCode(resultCode); - apiResponse.setMessage(message); - } - - return apiResponse; + return termsService.termsAgreementsProcess(termsDTO); } } diff --git a/src/main/java/com/barogagi/terms/dto/TermsAgreeDTO.java b/src/main/java/com/barogagi/terms/dto/TermsAgreeDTO.java new file mode 100644 index 0000000..1947d9c --- /dev/null +++ b/src/main/java/com/barogagi/terms/dto/TermsAgreeDTO.java @@ -0,0 +1,12 @@ +package com.barogagi.terms.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TermsAgreeDTO { + private String membershipNo = ""; + private int termsNum = 0; + private String agreeYn = ""; +} diff --git a/src/main/java/com/barogagi/terms/dto/TermsDTO.java b/src/main/java/com/barogagi/terms/dto/TermsDTO.java index 37fd45b..e20c5c0 100644 --- a/src/main/java/com/barogagi/terms/dto/TermsDTO.java +++ b/src/main/java/com/barogagi/terms/dto/TermsDTO.java @@ -12,9 +12,7 @@ @Getter @Setter public class TermsDTO extends DefaultVO { - private int termsNum = 0; private String userId = ""; - private String agreeYn = ""; @ArraySchema(schema = @Schema(implementation = TermsProcessDTO.class)) private List termsAgreeList = new ArrayList<>(); diff --git a/src/main/java/com/barogagi/terms/dto/TermsProcessDTO.java b/src/main/java/com/barogagi/terms/dto/TermsProcessDTO.java index 63c2f61..cd6b9fc 100644 --- a/src/main/java/com/barogagi/terms/dto/TermsProcessDTO.java +++ b/src/main/java/com/barogagi/terms/dto/TermsProcessDTO.java @@ -8,7 +8,5 @@ @Setter public class TermsProcessDTO { private int termsNum = 0; - private String userId = ""; private String agreeYn = ""; - private String membershipNo = ""; } diff --git a/src/main/java/com/barogagi/terms/exception/TermsException.java b/src/main/java/com/barogagi/terms/exception/TermsException.java new file mode 100644 index 0000000..ed4fc81 --- /dev/null +++ b/src/main/java/com/barogagi/terms/exception/TermsException.java @@ -0,0 +1,13 @@ +package com.barogagi.terms.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.util.exception.ErrorCode; +import lombok.Getter; + +@Getter +public class TermsException extends BusinessException { + + public TermsException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/barogagi/terms/mapper/TermsMapper.java b/src/main/java/com/barogagi/terms/mapper/TermsMapper.java index c12e3d6..2649286 100644 --- a/src/main/java/com/barogagi/terms/mapper/TermsMapper.java +++ b/src/main/java/com/barogagi/terms/mapper/TermsMapper.java @@ -1,5 +1,6 @@ package com.barogagi.terms.mapper; +import com.barogagi.terms.dto.TermsAgreeDTO; import com.barogagi.terms.dto.TermsInputDTO; import com.barogagi.terms.dto.TermsOutputDTO; import com.barogagi.terms.dto.TermsProcessDTO; @@ -12,5 +13,5 @@ public interface TermsMapper { // 사용중인 약관 목록 조회 List selectTermsList(TermsInputDTO termsInputDTO); - int insertTermsAgreeInfo(TermsProcessDTO vo); + int insertTermsAgreeInfo(TermsAgreeDTO vo); } diff --git a/src/main/java/com/barogagi/terms/service/TermsService.java b/src/main/java/com/barogagi/terms/service/TermsService.java index 22c4fbe..169ce78 100644 --- a/src/main/java/com/barogagi/terms/service/TermsService.java +++ b/src/main/java/com/barogagi/terms/service/TermsService.java @@ -1,41 +1,131 @@ package com.barogagi.terms.service; +import com.barogagi.member.login.dto.LoginVO; +import com.barogagi.member.login.service.LoginService; +import com.barogagi.response.ApiResponse; +import com.barogagi.terms.dto.*; +import com.barogagi.terms.exception.TermsException; import com.barogagi.terms.mapper.TermsMapper; -import com.barogagi.terms.dto.TermsInputDTO; -import com.barogagi.terms.dto.TermsOutputDTO; -import com.barogagi.terms.dto.TermsProcessDTO; +import com.barogagi.util.InputValidate; +import com.barogagi.util.Validator; +import com.barogagi.util.exception.ErrorCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAspectSupport; +import java.util.ArrayList; import java.util.List; @Service public class TermsService { - private TermsMapper termsMapper; + private final TermsMapper termsMapper; + private final Validator validator; + private final InputValidate inputValidate; + private final LoginService loginService; @Autowired - public TermsService(TermsMapper termsMapper) { + public TermsService( + TermsMapper termsMapper, + Validator validator, + InputValidate inputValidate, + LoginService loginService + ) + { this.termsMapper = termsMapper; + this.validator = validator; + this.inputValidate = inputValidate; + this.loginService = loginService; + } + + public ApiResponse termsListProcess(String apiSecretKey, String termsType) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(apiSecretKey)) { + throw new TermsException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(termsType)) { + throw new TermsException(ErrorCode.EMPTY_DATA); + } + + // 3. 약관 조회 + TermsInputDTO termsInputDTO = new TermsInputDTO(); + termsInputDTO.setTermsType(termsType); + List termsList = this.selectTermsList(termsInputDTO); + + if(termsList.isEmpty()) { + throw new TermsException(ErrorCode.NOT_FOUND_TERMS); + + } + + return ApiResponse.resultData( + termsList, + ErrorCode.FOUND_TERMS.getCode(), + ErrorCode.FOUND_TERMS.getMessage() + ); + } + + public ApiResponse termsAgreementsProcess(TermsDTO termsDTO) { + + // 1. API SECRET KEY 일치 여부 확인 + if(!validator.apiSecretKeyCheck(termsDTO.getApiSecretKey())) { + throw new TermsException(ErrorCode.NOT_EQUAL_API_SECRET_KEY); + } + + // 2. 필수 입력값 확인 + if(inputValidate.isEmpty(termsDTO.getUserId()) || + termsDTO.getTermsAgreeList() == null || + termsDTO.getTermsAgreeList().isEmpty()) { + throw new TermsException(ErrorCode.EMPTY_DATA); + } + + LoginVO lvo = new LoginVO(); + lvo.setUserId(termsDTO.getUserId()); + LoginVO loginVO = loginService.findMembershipNo(lvo); + if(null == loginVO) { + throw new TermsException(ErrorCode.NOT_FOUND_USER_INFO); + } + + List termsAgreeDTOList = new ArrayList<>(); + List termsAgreeList = termsDTO.getTermsAgreeList(); + + for(TermsProcessDTO termsProcessDTO : termsAgreeList) { + TermsAgreeDTO termsAgreeDTO = new TermsAgreeDTO(); + termsAgreeDTO.setMembershipNo(loginVO.getMembershipNo()); + termsAgreeDTO.setTermsNum(termsProcessDTO.getTermsNum()); + termsAgreeDTO.setAgreeYn(termsProcessDTO.getAgreeYn()); + termsAgreeDTOList.add(termsAgreeDTO); + } + String resCode = this.insertTermsAgreeList(termsAgreeDTOList); + + if(!resCode.equals("200")) { + throw new TermsException(ErrorCode.FAIL_INSERT_TERMS); + } + + return ApiResponse.result( + ErrorCode.SUCCESS_INSERT_TERMS.getCode(), + ErrorCode.SUCCESS_INSERT_TERMS.getMessage() + ); } // 사용중인 약관 목록 조회 - public List selectTermsList(TermsInputDTO termsInputDTO) throws Exception { + public List selectTermsList(TermsInputDTO termsInputDTO) { return termsMapper.selectTermsList(termsInputDTO); } // 약관 동의 여부 저장 - public int insertTermsAgreeInfo(TermsProcessDTO vo) throws Exception { + public int insertTermsAgreeInfo(TermsAgreeDTO vo) { return termsMapper.insertTermsAgreeInfo(vo); } @Transactional - public String insertTermsAgreeList(List termsList) { + public String insertTermsAgreeList(List termsList) { String resultCode = ""; try { - for(TermsProcessDTO vo : termsList) { + for(TermsAgreeDTO vo : termsList) { int insertFlag = this.insertTermsAgreeInfo(vo); if(insertFlag > 0){ resultCode = "200"; diff --git a/src/main/java/com/barogagi/util/HtmlUtils.java b/src/main/java/com/barogagi/util/HtmlUtils.java new file mode 100644 index 0000000..2f6d1da --- /dev/null +++ b/src/main/java/com/barogagi/util/HtmlUtils.java @@ -0,0 +1,37 @@ +package com.barogagi.util; + +import java.util.regex.Pattern; + +public class HtmlUtils { + + private static final Pattern HTML_TAG = Pattern.compile("<[^>]*>"); + + private HtmlUtils() { + // util 클래스는 인스턴스 생성 방지 + } + + public static String stripHtml(String s) { + if (s == null || s.isBlank()) return ""; + // 1) 태그 제거 + String t = HTML_TAG.matcher(s).replaceAll(" "); + // 2) 엔티티 정리 + t = t.replace(" ", " "); + // 3) 공백 정리 + return t.replaceAll("\\s+", " ").trim(); + } + + + /** ```json ... ``` 형태 코드펜스/공백 제거 */ + public static String stripCodeFence(String s) { + if (s == null) return null; + // 앞쪽 ``` 또는 ```json, 뒤쪽 ``` 제거 + String t = s.trim(); + if (t.startsWith("```")) { + t = t.replaceFirst("^```(?:json)?\\s*", ""); + } + if (t.endsWith("```")) { + t = t.replaceFirst("```\\s*$", ""); + } + return t.trim(); + } +} diff --git a/src/main/java/com/barogagi/util/MembershipUtil.java b/src/main/java/com/barogagi/util/MembershipUtil.java new file mode 100644 index 0000000..92c1959 --- /dev/null +++ b/src/main/java/com/barogagi/util/MembershipUtil.java @@ -0,0 +1,53 @@ +package com.barogagi.util; + +import com.barogagi.util.exception.BasicException; +import com.barogagi.util.exception.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class MembershipUtil { + + public Map membershipNoService(HttpServletRequest request) { + + Map resultMap = new HashMap<>(); + + String resultCode = ""; + String message = ""; + String membershipNo = ""; + + try { + Object membershipNoAttr = request.getAttribute("membershipNo"); + if(membershipNoAttr == null) { + throw new BasicException(ErrorCode.NOT_EXIST_ACCESS_AUTH); + } + + membershipNo = String.valueOf(membershipNoAttr); + resultCode = ErrorCode.EXIST_ACCESS_AUTH.getCode(); + message = ErrorCode.EXIST_ACCESS_AUTH.getMessage(); + + } catch (BasicException ex) { + resultCode = ex.getCode(); + message = ex.getMessage(); + } finally { + resultMap.put("resultCode", resultCode); + resultMap.put("message", message); + resultMap.put("membershipNo", membershipNo); + } + + return resultMap; + } + + public String selectMembershipNo(HttpServletRequest request) { + + Object membershipNoAttr = request.getAttribute("membershipNo"); + if(null == membershipNoAttr) { + throw new BasicException(ErrorCode.NOT_EXIST_ACCESS_AUTH); + } + + return String.valueOf(membershipNoAttr); + } +} diff --git a/src/main/java/com/barogagi/util/Validator.java b/src/main/java/com/barogagi/util/Validator.java index e81a49b..5949a3b 100644 --- a/src/main/java/com/barogagi/util/Validator.java +++ b/src/main/java/com/barogagi/util/Validator.java @@ -1,5 +1,7 @@ package com.barogagi.util; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; import java.util.regex.Pattern; @@ -10,6 +12,18 @@ public class Validator { // 금칙어 목록 예시 (확장 가능) private static final String[] BLOCKED_WORDS = {"admin", "운영자"}; + private final String API_SECRET_KEY; + + @Autowired + public Validator(Environment environment) { + this.API_SECRET_KEY = environment.getProperty("api.secret-key"); + } + + // API SECRET KEY 검증 + public boolean apiSecretKeyCheck(String apiSecretKey) { + return apiSecretKey.equals(API_SECRET_KEY); + } + // 아이디 검증 public boolean isValidId(String userId) { diff --git a/src/main/java/com/barogagi/util/exception/BasicException.java b/src/main/java/com/barogagi/util/exception/BasicException.java new file mode 100644 index 0000000..196f0c6 --- /dev/null +++ b/src/main/java/com/barogagi/util/exception/BasicException.java @@ -0,0 +1,16 @@ +package com.barogagi.util.exception; + +import com.barogagi.config.exception.BusinessException; +import lombok.Getter; + +@Getter +public class BasicException extends BusinessException { + + private final ErrorCode errorCode; + + // 신규 생성자 + public BasicException(ErrorCode errorCode) { + super(errorCode); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/barogagi/util/exception/ErrorCode.java b/src/main/java/com/barogagi/util/exception/ErrorCode.java new file mode 100644 index 0000000..565dbd5 --- /dev/null +++ b/src/main/java/com/barogagi/util/exception/ErrorCode.java @@ -0,0 +1,115 @@ +package com.barogagi.util.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // API_SECRET_KEY 일치 X + NOT_EQUAL_API_SECRET_KEY(HttpStatus.UNAUTHORIZED, "A100", "잘못된 접근입니다."), + + // ACCESS TOKEN + NOT_EXIST_ACCESS_AUTH(HttpStatus.UNAUTHORIZED, "A401", "접근 권한이 존재하지 않습니다."), + EXIST_ACCESS_AUTH(HttpStatus.OK, "A200", "회원 번호가 존재합니다."), + EXPIRE_TOKEN(HttpStatus.UNAUTHORIZED, "A300", "Token이 만료되었습니다."), + + // Common + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON-500", "서버 오류가 발생했습니다."), + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "COMMON-400", "잘못된 요청입니다."), + EMPTY_DATA(HttpStatus.BAD_REQUEST, "C101", "정보를 입력해주세요."), + + // Membership + MEMBERSHIP_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBERSHIP-404", "멤버십 정보가 없습니다."), + MEMBERSHIP_SERVICE_FAIL(HttpStatus.BAD_GATEWAY, "MEMBERSHIP-502", "멤버십 서비스 호출에 실패했습니다."), + + // Schedule + SCHEDULE_SAVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "SCH-001", "일정 저장에 실패했습니다."), + SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "SCH-002", "일정 정보를 찾을 수 없습니다."), + SCHEDULE_ALREADY_DELETED(HttpStatus.NOT_FOUND, "SCH-003", "이미 삭제된 일정입니다."), + + // Tag + TAG_NOT_FOUND(HttpStatus.NOT_FOUND, "TAG-001", "태그 정보를 찾을 수 없습니다."), + + // Item + ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "ITEM-001", "아이템 정보를 찾을 수 없습니다."), + + // Region + REGION_NOT_FOUND(HttpStatus.NOT_FOUND, "REGION-001", "지역 정보를 찾을 수 없습니다."), + + // Nickname + INVALID_NICKNAME(HttpStatus.BAD_REQUEST, "N102", "적합하지 않는 닉네임입니다."), + UNAVAILABLE_NICKNAME(HttpStatus.CONFLICT, "N103", "해당 닉네임 사용이 불가능합니다."), + AVAILABLE_NICKNAME(HttpStatus.OK, "N200", "사용 가능한 닉네임입니다."), + + // UserId + INVALID_USER_ID(HttpStatus.BAD_REQUEST, "U102", "적합한 아이디가 아닙니다."), + UNAVAILABLE_USER_ID(HttpStatus.CONFLICT, "U300", "해당 아이디 사용이 불가능합니다."), + AVAILABLE_USER_ID(HttpStatus.OK, "U200", "해당 아이디 사용이 가능합니다."), + + // SignUp + INVALID_SIGN_UP(HttpStatus.BAD_REQUEST, "S102", "적합한 아이디, 비밀번호, 닉네임이 아닙니다."), + SUCCESS_SIGN_UP(HttpStatus.CREATED, "S200", "회원가입에 성공하였습니다."), + FAIL_SIGN_UP(HttpStatus.INTERNAL_SERVER_ERROR, "S300", "회원가입에 실패하였습니다."), + + // DeleteAccount + SUCCESS_DELETE_ACCOUNT(HttpStatus.OK, "D200", "회원 탈퇴되었습니다."), + FAIL_DELETE_ACCOUNT(HttpStatus.INTERNAL_SERVER_ERROR, "D300", "회원 탈퇴 실패하였습니다."), + + // FindUser + FOUND_ACCOUNT(HttpStatus.OK, "F200", "해당 전화번호로 가입된 아이디가 존재합니다."), + NOT_FOUND_ACCOUNT(HttpStatus.NOT_FOUND, "F201", "해당 전화번호로 가입된 계정이 존재하지 않습니다."), + + // UpdatePassword + SUCCESS_UPDATE_PASSWORD(HttpStatus.OK, "U200", "비밀번호 재설정에 성공하였습니다."), + FAIL_UPDATE_PASSWORD(HttpStatus.INTERNAL_SERVER_ERROR, "U300", "비밀번호 재설정에 실패하였습니다."), + + // Login + NOT_FOUND_USER_INFO(HttpStatus.NOT_FOUND, "L102", "회원 정보가 존재하지 않습니다."), + FAIL_LOGIN(HttpStatus.UNAUTHORIZED, "L103", "로그인에 실패하였습니다."), + SUCCESS_LOGIN(HttpStatus.OK, "L200", "로그인에 성공하였습니다."), + + // RefreshToken + REQUIRED_LOGIN(HttpStatus.UNAUTHORIZED, "R110", "로그인을 진행해주세요."), + REQUIRED_RE_LOGIN(HttpStatus.UNAUTHORIZED, "R120", "로그인을 다시 진행해주세요."), + SUCCESS_REFRESH_TOKEN(HttpStatus.OK, "R200", "토큰이 발급되었습니다."), + FAIL_REFRESH_TOKEN(HttpStatus.INTERNAL_SERVER_ERROR, "R130", "토큰 발급에 실패하였습니다."), + UNAVAILABLE_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "R301", "유효하지 않은 refresh token입니다."), + NOT_FOUND_AVAILABLE_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "R302", "유효한 token 정보를 찾을 수 없습니다."), + + // Logout + FAIL_LOGOUT(HttpStatus.INTERNAL_SERVER_ERROR, "L300", "로그아웃 실패하였습니다."), + SUCCESS_LOGOUT(HttpStatus.OK, "L200", "로그아웃 되었습니다."), + + // Terms + FOUND_TERMS(HttpStatus.OK, "T200", "약관 조회에 성공하였습니다."), + NOT_FOUND_TERMS(HttpStatus.NOT_FOUND, "T102", "약관이 존재하지 않습니다."), + SUCCESS_INSERT_TERMS(HttpStatus.CREATED, "T200", "약관 저장에 성공하였습니다."), + FAIL_INSERT_TERMS(HttpStatus.INTERNAL_SERVER_ERROR, "T300", "약관 저장에 실패하였습니다."), + + // MemberInfo + FOUND_USER_INFO(HttpStatus.OK, "M200", "회원 정보 조회가 완료되었습니다."), + FAIL_UPDATE_USER_INFO(HttpStatus.INTERNAL_SERVER_ERROR, "M404", "사용자 정보 수정 실패하였습니다."), + SUCCESS_UPDATE_USER_INFO(HttpStatus.OK, "M200", "사용자 정보 수정 완료하였습니다."), + + // MainPage + NOT_FOUND_SCHEDULE(HttpStatus.NOT_FOUND, "M201", "일정이 존재하지 않습니다."), + FOUND_SCHEDULE(HttpStatus.OK, "M200", "조회 성공하였습니다."), + NOT_FOUND_POPULAR_TAG(HttpStatus.NOT_FOUND, "M201", "인기 태그 목록이 존재하지 않습니다."), + FOUND_POPULAR_TAG(HttpStatus.OK, "M200", "인기 태그 조회 완료하였습니다."), + NOT_FOUND_POPULAR_REGION(HttpStatus.NOT_FOUND, "M201", "인기 지역 목록이 존재하지 않습니다."), + FOUND_POPULAR_REGION(HttpStatus.OK, "M200", "인기 지역 조회 완료하였습니다."), + + // Approval + SUCCESS_SEND_SMS(HttpStatus.OK, "A200", "인증번호 발송에 성공하었습니다."), + FAIL_SEND_SMS(HttpStatus.INTERNAL_SERVER_ERROR, "A103", "인증번호 발송에 실패하였습니다."), + ERROR_SEND_SMS(HttpStatus.INTERNAL_SERVER_ERROR, "A102", "인증문자 발송 중 오류가 발생하였습니다."), + SUCCESS_CHECK_SMS(HttpStatus.OK, "A200", "인증이 완료되었습니다."), + FAIL_CHECK_SMS(HttpStatus.BAD_REQUEST, "A300", "인증에 실패하였습니다."); + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/barogagi/util/exception/GlobalExceptionHandler.java b/src/main/java/com/barogagi/util/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..8bd8a60 --- /dev/null +++ b/src/main/java/com/barogagi/util/exception/GlobalExceptionHandler.java @@ -0,0 +1,40 @@ +package com.barogagi.util.exception; + +import com.barogagi.config.exception.BusinessException; +import com.barogagi.response.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BasicException.class) + public ResponseEntity> handleBasicException(BasicException e) { + + ErrorCode errorCode = e.getErrorCode(); + + return ResponseEntity + .status(errorCode.getStatus()) + .body(ApiResponse.error( + errorCode.getCode(), + errorCode.getMessage() + )); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnknown(Exception e) { + + return ResponseEntity + .status(ErrorCode.INTERNAL_ERROR.getStatus()) + .body(ApiResponse.error( + ErrorCode.INTERNAL_ERROR.getCode(), + ErrorCode.INTERNAL_ERROR.getMessage() + )); + } + + @ExceptionHandler(BusinessException.class) + public ApiResponse handleBusinessException(BusinessException e) { + return ApiResponse.result(e.getCode(), e.getMessage()); + } +} diff --git a/src/main/resources/mapper/AuthMapper.xml b/src/main/resources/mapper/AuthMapper.xml new file mode 100644 index 0000000..00a2901 --- /dev/null +++ b/src/main/resources/mapper/AuthMapper.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/main/resources/mapper/CategoryMapper.xml b/src/main/resources/mapper/CategoryMapper.xml new file mode 100644 index 0000000..681a544 --- /dev/null +++ b/src/main/resources/mapper/CategoryMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/ItemMapper.xml b/src/main/resources/mapper/ItemMapper.xml index 5307fd8..88746f5 100644 --- a/src/main/resources/mapper/ItemMapper.xml +++ b/src/main/resources/mapper/ItemMapper.xml @@ -6,5 +6,12 @@ - + diff --git a/src/main/resources/mapper/JoinMapper.xml b/src/main/resources/mapper/JoinMapper.xml index be80a85..4b73d5e 100644 --- a/src/main/resources/mapper/JoinMapper.xml +++ b/src/main/resources/mapper/JoinMapper.xml @@ -3,12 +3,12 @@ - - - + + = CURDATE() + ) ss + WHERE ss.rn = 1 + ) cs + LEFT OUTER JOIN ( + SELECT * + FROM ( + SELECT p.*, + ROW_NUMBER() OVER (PARTITION BY p.SCHEDULE_NUM ORDER BY p.START_TIME ASC, p.PLAN_NUM ASC) AS rn + FROM PLAN p + WHERE p.DEL_YN = 'N' + ) px + WHERE px.rn = 1 + ) p + ON cs.SCHEDULE_NUM = p.SCHEDULE_NUM + LEFT OUTER JOIN ITEM i + ON p.ITEM_NUM = i.ITEM_NUM + LEFT OUTER JOIN CATEGORY c + ON i.CATEGORY_NUM = c.CATEGORY_NUM + ORDER BY cs.START_DATE DESC + ]]> + + + + + + + + + + diff --git a/src/main/resources/mapper/MemberMapper.xml b/src/main/resources/mapper/MemberMapper.xml index a518aa7..e6cd44c 100644 --- a/src/main/resources/mapper/MemberMapper.xml +++ b/src/main/resources/mapper/MemberMapper.xml @@ -10,7 +10,6 @@ BIRTH as birth, TEL as tel, GENDER as gender, - PROFILE_IMG as profileImg, NICKNAME as nickName, JOIN_TYPE as joinType, REG_DATE as regDate, @@ -31,7 +30,6 @@ BIRTH as birth, TEL as tel, GENDER as gender, - PROFILE_IMG as profileImg, NICKNAME as nickName, JOIN_TYPE as joinType, REG_DATE as regDate, @@ -46,13 +44,9 @@ diff --git a/src/main/resources/mapper/RegionMapper.xml b/src/main/resources/mapper/RegionMapper.xml index eef2454..9513361 100644 --- a/src/main/resources/mapper/RegionMapper.xml +++ b/src/main/resources/mapper/RegionMapper.xml @@ -21,4 +21,94 @@ WHERE a.PLAN_NUM = #{planNum}; ]]> + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/ScheduleMapper.xml b/src/main/resources/mapper/ScheduleMapper.xml index 55366e0..1467d1a 100644 --- a/src/main/resources/mapper/ScheduleMapper.xml +++ b/src/main/resources/mapper/ScheduleMapper.xml @@ -5,9 +5,39 @@ + + + + + + + + + + + + - + + + + diff --git a/src/main/resources/mapper/TagMapper.xml b/src/main/resources/mapper/TagMapper.xml index 4026c61..c6155b3 100644 --- a/src/main/resources/mapper/TagMapper.xml +++ b/src/main/resources/mapper/TagMapper.xml @@ -11,9 +11,47 @@ SELECT b.TAG_NUM as tagNum , b.TAG_NM as tagNm + , b.TAG_TYPE as tagType + , b.CATEGORY_NUM as categoryNum FROM PLAN_TAG a JOIN TAG b ON a.TAG_NUM = b.TAG_NUM WHERE a.PLAN_NUM = #{planNum}; ]]> + + + + + + + diff --git a/src/main/resources/mapper/TermsMapper.xml b/src/main/resources/mapper/TermsMapper.xml index cbf01d2..a6e42ba 100644 --- a/src/main/resources/mapper/TermsMapper.xml +++ b/src/main/resources/mapper/TermsMapper.xml @@ -18,7 +18,7 @@ ]]> - +