From 8bf6c3f9c66eca11eb7fc0246c76a6dd32a4c330 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 1 May 2025 22:48:06 +0900 Subject: [PATCH 001/120] =?UTF-8?q?chore:=20BaseEntity=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/common/BaseEntity.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/common/BaseEntity.java diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java new file mode 100644 index 0000000..1f10be8 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java @@ -0,0 +1,22 @@ +package com.stcom.smartmealtable.common; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; + +} \ No newline at end of file From 7718ece3a1db6c85629eec11b51e5209de05a85b Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 1 May 2025 22:48:37 +0900 Subject: [PATCH 002/120] =?UTF-8?q?feat(MemberPassword):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/Member.java | 37 ++++++++ .../domain/member/MemberPassword.java | 93 +++++++++++++++++++ .../PasswordFailedExceededException.java | 7 ++ 3 files changed, 137 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/Member.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java new file mode 100644 index 0000000..52ed0ac --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -0,0 +1,37 @@ +package com.stcom.smartmealtable.domain.member; + +import com.stcom.smartmealtable.common.BaseEntity; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; + +@Entity +@Table(name = "MEMBER_AUTH") +public class Member extends BaseEntity { + + @Id + @GeneratedValue + private Long id; + + @Email + private String email; + + @Embedded + private MemberPassword password; + + private boolean isEmailVerified; + + public void changePassword(String rawOldPassword, String rawNewPassword) { + if (rawOldPassword.isBlank() || rawNewPassword.isBlank()) { + throw new IllegalArgumentException("빈 비밀번호를 입력했습니다"); + } + password.changePassword(rawOldPassword, rawNewPassword); + } + + public boolean isMatchedPassword(final String rawPassword) throws PasswordFailedExceededException { + return password.isMatched(rawPassword); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java new file mode 100644 index 0000000..045dc2b --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -0,0 +1,93 @@ +package com.stcom.smartmealtable.domain.member; + + +import jakarta.persistence.Embeddable; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.NoArgsConstructor; + +@Embeddable +@NoArgsConstructor +public class MemberPassword { + + private final static int MAX_FAILED_COUNT = 5; + private final static long TTL = 1209_604; // 2 weeks + private String password_hash; + + private int failedCount; + + private LocalDateTime expirationDate; + + private long ttl; + + @Builder + public MemberPassword(String rawPassword) { + this.password_hash = rawPassword; + this.expirationDate = LocalDateTime.now().plusSeconds(TTL); + this.ttl = TTL; // 2 weeks + this.failedCount = 0; + } + + public boolean isMatched(final String rawPassword) throws PasswordFailedExceededException { + checkFailedCount(); + final boolean matches = isMatches(rawPassword); + updateFailedCount(matches); + return matches; + } + + public boolean isPasswordExpired() { + return LocalDateTime.now().isAfter(expirationDate); + } + + private void checkFailedCount() throws PasswordFailedExceededException { + if (failedCount >= MAX_FAILED_COUNT) { + throw new PasswordFailedExceededException(); + } + } + + private boolean isMatches(String rawPassword) { + return password_hash.equals(encodePassword(rawPassword)); + } + + private void updateFailedCount(boolean matches) { + if (matches) { + failedCount = 0; + return; + } + + failedCount++; + } + + public void changePassword(final String newPassword, final String oldPassword) { + if (isMatches(oldPassword)) { + password_hash = encodePassword(newPassword); + extendExpirationDate(); + } + } + + private void extendExpirationDate() { + expirationDate = LocalDateTime.now().plusSeconds(ttl); + } + + private String encodePassword(String newPassword) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(newPassword.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(Character.forDigit((b >> 4) & 0xF, 16)) + .append(Character.forDigit((b & 0xF), 16)); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java new file mode 100644 index 0000000..29972ee --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.domain.member; + +public class PasswordFailedExceededException extends Exception { + public PasswordFailedExceededException() { + super("비밀번호 실패 횟수가 5회를 초과하였습니다."); + } +} From 47b4b6df8d03b9c922d41f08017d34d39383c263 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 1 May 2025 23:40:23 +0900 Subject: [PATCH 003/120] =?UTF-8?q?feat(memberPassword):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberPassword.java | 24 ++++++++++++++++-- .../member/PasswordPolicyException.java | 25 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java index 045dc2b..34417c2 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -15,6 +15,7 @@ public class MemberPassword { private final static int MAX_FAILED_COUNT = 5; private final static long TTL = 1209_604; // 2 weeks + private String password_hash; private int failedCount; @@ -24,13 +25,32 @@ public class MemberPassword { private long ttl; @Builder - public MemberPassword(String rawPassword) { - this.password_hash = rawPassword; + public MemberPassword(String rawPassword) throws PasswordPolicyException { + checkPasswordPolicy(rawPassword); + this.password_hash = encodePassword(rawPassword); this.expirationDate = LocalDateTime.now().plusSeconds(TTL); this.ttl = TTL; // 2 weeks this.failedCount = 0; } + public void checkPasswordPolicy(String rawPassword) throws PasswordPolicyException { + if (rawPassword.contains(" ")) { + throw new PasswordPolicyException("비밀번호는 공백을 포함할 수 없습니다"); + } + + if (rawPassword.length() < 8) { + throw new PasswordPolicyException("비밀번호는 8자 이상이어야 합니다."); + } + + if (rawPassword.length() > 20) { + throw new PasswordPolicyException("비밀번호는 최대 20자까지 가능합니다."); + } + + if (!rawPassword.matches("^[A-Za-z0-9]+$")) { + throw new PasswordPolicyException("비밀번호는 영문자(A–Z, a–z)와 숫자(0–9)로만 구성되어야 합니다."); + } + } + public boolean isMatched(final String rawPassword) throws PasswordFailedExceededException { checkFailedCount(); final boolean matches = isMatches(rawPassword); diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java new file mode 100644 index 0000000..94b453d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java @@ -0,0 +1,25 @@ +package com.stcom.smartmealtable.domain.member; + +public class PasswordPolicyException extends Exception { + + public PasswordPolicyException() { + super(); + } + + public PasswordPolicyException(String message) { + super(message); + } + + public PasswordPolicyException(String message, Throwable cause) { + super(message, cause); + } + + public PasswordPolicyException(Throwable cause) { + super(cause); + } + + protected PasswordPolicyException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} From 5281311d0b9aa7bfca99f9a5b52684fe1ec68799 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 01:34:38 +0900 Subject: [PATCH 004/120] =?UTF-8?q?fix(memberPassword):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비밀번호 변경 시 새로운 비밀번호에 대한 Policy 검증 부분을 추가하였습니다. --- .../smartmealtable/domain/member/Member.java | 3 ++- .../domain/member/MemberPassword.java | 14 ++++++++++---- .../member/PasswordFailedExceededException.java | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 52ed0ac..a0c1b69 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -24,7 +24,8 @@ public class Member extends BaseEntity { private boolean isEmailVerified; - public void changePassword(String rawOldPassword, String rawNewPassword) { + public void changePassword(String rawOldPassword, String rawNewPassword) + throws PasswordFailedExceededException, PasswordPolicyException { if (rawOldPassword.isBlank() || rawNewPassword.isBlank()) { throw new IllegalArgumentException("빈 비밀번호를 입력했습니다"); } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java index 34417c2..07f3ba7 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -81,11 +81,17 @@ private void updateFailedCount(boolean matches) { failedCount++; } - public void changePassword(final String newPassword, final String oldPassword) { - if (isMatches(oldPassword)) { - password_hash = encodePassword(newPassword); - extendExpirationDate(); + public void changePassword(final String newPassword, final String oldPassword) + throws PasswordFailedExceededException, PasswordPolicyException { + if (!isMatches(oldPassword)) { + throw new PasswordFailedExceededException("기존 비밀번호가 일치하지 않습니다"); } + if (newPassword.equals(oldPassword)) { + throw new PasswordPolicyException("기존 비밀번호와 새 비밀번호가 동일합니다. 기존 비밀번호와 동일하지 않은 비밀번호로 설정해주세요"); + } + checkPasswordPolicy(newPassword); + password_hash = encodePassword(newPassword); + extendExpirationDate(); } private void extendExpirationDate() { diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java index 29972ee..ddea7ef 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java @@ -4,4 +4,21 @@ public class PasswordFailedExceededException extends Exception { public PasswordFailedExceededException() { super("비밀번호 실패 횟수가 5회를 초과하였습니다."); } + + public PasswordFailedExceededException(String message) { + super(message); + } + + public PasswordFailedExceededException(String message, Throwable cause) { + super(message, cause); + } + + public PasswordFailedExceededException(Throwable cause) { + super(cause); + } + + protected PasswordFailedExceededException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } } From d9d956419f40db6c798f6c0afcca65c374157671 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 01:34:53 +0900 Subject: [PATCH 005/120] =?UTF-8?q?test(memberPassword):=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberPasswordTest.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java new file mode 100644 index 0000000..0a69693 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java @@ -0,0 +1,109 @@ +package com.stcom.smartmealtable.domain.member; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MemberPasswordTest { + + @Test + void 비밀번호_정책_성공() throws Exception { + String successRawPassword = "abcdefg123"; + assertDoesNotThrow(() -> + MemberPassword.builder().rawPassword(successRawPassword).build() + ); + } + + @Test + void 비밀번호_정책_8자이상() throws Exception { + String failedRawPassword = "abc123"; + // JUnit5 + checkFailedCase(failedRawPassword); + } + + private void checkFailedCase(String failedRawPassword) { + assertThrows(PasswordPolicyException.class, () -> + MemberPassword.builder().rawPassword(failedRawPassword).build() + ); + } + + @Test + void 비밀번호_정책_공백() throws Exception { + String failedRawPassword = "aa bb124gf"; + checkFailedCase(failedRawPassword); + } + + @Test + void 비밀번호_정책_20자이하() throws Exception { + String failedRawPassword = "aasdafsf124e124241414114214"; + checkFailedCase(failedRawPassword); + } + + @Test + void 비밀번호_정책_영어숫자포함() throws Exception { + String failedRawPassword = "가나다라4asfsadfasd"; + checkFailedCase(failedRawPassword); + } + + + @Test + void 비밀번호_일치() throws Exception { + // given + MemberPassword newPassword = MemberPassword.builder() + .rawPassword("abcdefg1234") + .build(); + // then + assertThat(newPassword.isMatched("abcdefg1234")); + } + + @Test + void 비밀번호_변경_실패_이전비밀번호_불일치() throws Exception { + MemberPassword oldPassword + = MemberPassword.builder() + .rawPassword("abcdefg1234") + .build(); + + assertThrows(PasswordFailedExceededException.class, + () -> oldPassword.changePassword("abcdccc1234", "abcdefg123")); // 실패해야 함.) + } + + @Test + void 비밀번호_변경_실패_새비밀번호_정책실패() throws Exception { + MemberPassword oldPassword + = MemberPassword.builder() + .rawPassword("abcdefg1234") + .build(); + + assertThrows(PasswordPolicyException.class, + () -> oldPassword.changePassword("abcde", "abcdefg1234")); // 실패해야 함.) + } + + @Test + void 비밀번호_변경_실패_새비밀번호_기존과동일() throws Exception { + MemberPassword oldPassword + = MemberPassword.builder() + .rawPassword("abcdefg1234") + .build(); + + assertThrows(PasswordPolicyException.class, + () -> oldPassword.changePassword("abcdefg1234", "abcdefg1234")); // 실패해야 함.) + } + + + @Test + void 비밀번호_변경_성공() throws Exception { + MemberPassword password + = MemberPassword.builder() + .rawPassword("abcdefg1234") + .build(); + + assertDoesNotThrow( + () -> password.changePassword("aaabcd1234", "abcdefg1234")); // 실패해야 함.) + assertTrue(password.isMatched("aaabcd1234")); + } + + +} \ No newline at end of file From 5847518a85e70ac1ccc39b057e6bb06056a25ba5 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 21:47:38 +0900 Subject: [PATCH 006/120] =?UTF-8?q?docs(table):=20DB=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- table.ddl | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 table.ddl diff --git a/table.ddl b/table.ddl new file mode 100644 index 0000000..ea144ad --- /dev/null +++ b/table.ddl @@ -0,0 +1,217 @@ +-- 1. 회원 관련 테이블 + +-- 1.1. 회원 인증 테이블 (MemberAuth) +CREATE TABLE IF NOT EXISTS member_auth ( + user_id BIGINT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) DEFAULT NULL, + password_hash VARCHAR(255) DEFAULT NULL, + is_email_verified BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (user_id), + UNIQUE KEY uq_email (email) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 1.2. 회원 프로필 테이블 (MemberProfile) +CREATE TABLE IF NOT EXISTS member_profile ( + profile_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + full_name VARCHAR(255) NOT NULL, + default_image VARCHAR(255) DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (profile_id), + CONSTRAINT fk_memberprofile_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 1.3. 소셜 로그인 테이블 (SocialLogin) +CREATE TABLE IF NOT EXISTS social_login ( + social_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + provider VARCHAR(50) NOT NULL, + provider_user_id VARCHAR(255) NOT NULL, + access_token VARCHAR(512) NOT NULL, + refresh_token VARCHAR(512) DEFAULT NULL, + token_expires_at TIMESTAMP NULL DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (social_id), + CONSTRAINT fk_sociallogin_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE KEY uq_provider_user (provider, provider_user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 1.4. 회원 주소 테이블 (MemberAddress) +CREATE TABLE IF NOT EXISTS member_address ( + address_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + address VARCHAR(255) NOT NULL, -- 기본 주소 + road_address VARCHAR(255) NOT NULL, -- 도로명 주소 + detail_address VARCHAR(255), -- 상세 주소 + alias VARCHAR(255), -- 주소 별칭 + latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 + longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 + status VARCHAR(255) NOT NULL, -- 주소 상태 (HOME, COMPANY, ETC, N:삭제) + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (address_id), + CONSTRAINT fk_memberaddress_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 2. 예산 관리 테이블 + +-- 2.1. 월별 예산 테이블 (MonthlyBudget) +CREATE TABLE IF NOT EXISTS monthly_budget ( + budget_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + year_month CHAR(7) NOT NULL, -- "YYYY-MM" 형식 (예: "2025-04") + monthly_limit DECIMAL(10,2) NOT NULL, -- 한 달 목표 예산 (예: 500000.00) + spent_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 지금까지 소비한 금액 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (budget_id), + UNIQUE KEY uq_user_yearmonth (user_id, year_month), + CONSTRAINT fk_monthlybudget_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2.2. 일별 예산 테이블 (DailyBudget) +CREATE TABLE IF NOT EXISTS daily_budget ( + daily_budget_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + budget_date DATE NOT NULL, + daily_limit DECIMAL(10,2) NOT NULL, -- 일일 목표 예산 (예: 20000.00) + spent_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 하루 동안 소비한 총액 + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (daily_budget_id), + UNIQUE KEY uq_user_date (user_id, budget_date), + CONSTRAINT fk_dailybudget_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 3. 음식점 및 음식 정보 테이블 + +-- 3.1. 음식점 테이블 (FoodStore) +CREATE TABLE IF NOT EXISTS food_store ( + food_store_id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(255) NOT NULL, -- 음식점 이름 + store_type ENUM('RESTAURANT', 'SCHOOL_CAFETERIA', 'CONVENIENCE_STORE') NOT NULL, -- 음식점 타입 + address VARCHAR(255) NOT NULL, -- 음식점 주소 + latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 + longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 + phone VARCHAR(50) DEFAULT NULL, -- 음식점 전화번호 + open_time TIME DEFAULT NULL, -- 오픈 시간 + close_time TIME DEFAULT NULL, -- 마감 시간 + external_id VARCHAR(255) DEFAULT NULL, -- 외부 음식점 ID(KAKAO) + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + status VARCHAR(1) NOT NULL, -- 음식점 상태 (Y:활성화, N:삭제) + PRIMARY KEY (food_store_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3.2. 음식 (메뉴) 테이블 (Food) +CREATE TABLE IF NOT EXISTS food ( + food_id BIGINT NOT NULL AUTO_INCREMENT, + food_store_id BIGINT NOT NULL, + name VARCHAR(255) NOT NULL, + category VARCHAR(100) NOT NULL, -- 음식 종류 + price DECIMAL(10,2) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + status VARCHAR(1) NOT NULL, -- 음식 상태 (Y:활성화, N:삭제) + PRIMARY KEY (food_id), + CONSTRAINT fk_food_foodstore FOREIGN KEY (food_store_id) + REFERENCES food_store(food_store_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 4. 즐겨찾기 테이블 + +-- 4.1. 즐겨찾는 음식점 테이블 (FavoriteFoodStore) +CREATE TABLE IF NOT EXISTS favorite_food_store ( + favorite_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + food_store_id BIGINT NOT NULL, + rank_order INT DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (favorite_id), + CONSTRAINT fk_favorite_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_favorite_foodstore FOREIGN KEY (food_store_id) + REFERENCES food_store(food_store_id) + ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE KEY uq_favorite (user_id, food_store_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 5. 지출 내역 테이블 + +-- 5.1. 지출 내역 테이블 (Expenditure) +CREATE TABLE IF NOT EXISTS expenditure ( + expenditure_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + food_store_id BIGINT DEFAULT NULL, + transaction_date DATETIME NOT NULL, + amount DECIMAL(10,2) NOT NULL, + source_type ENUM('MANUAL', 'SMS', 'CAPTURE', 'RECOMMENDATION', 'IN_APP') NOT NULL, + description TEXT DEFAULT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (expenditure_id), + CONSTRAINT fk_expenditure_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_expenditure_foodstore FOREIGN KEY (food_store_id) + REFERENCES food_store(food_store_id) + ON DELETE SET NULL ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 5.2. 주문서 ExpenditureFood +CREATE TABLE IF NOT EXISTS expenditure_food ( + expenditure_food_id BIGINT NOT NULL AUTO_INCREMENT, + expenditure_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + quantity INT NOT NULL DEFAULT 1, -- 주문한 개수 + unit_price DECIMAL(10,2) NOT NULL, -- 주문 당시 단위 가격 (Food 테이블의 가격과 동일할 수도 있으나, 주문 시점의 가격을 기록) + total_price DECIMAL(10,2) NOT NULL, -- quantity * unit_price 계산 값 (혹은 주문 당시 총액) + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (expenditure_food_id), + CONSTRAINT fk_expenditurefood_expenditure FOREIGN KEY (expenditure_id) + REFERENCES expenditure(expenditure_id) + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_expenditurefood_food FOREIGN KEY (food_id) + REFERENCES food(food_id) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + +-- 6. 사용자 음식 취향 테이블 + +-- 6.1. 사용자 음식 취향 선호 테이블 (UserFoodPreference) +CREATE TABLE IF NOT EXISTS user_food_preference ( + preference_id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + food_keyword VARCHAR(100) NOT NULL, + food_category VARCHAR(100) NOT NULL, + preference_order INT NOT NULL, + is_dislike BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (preference_id), + CONSTRAINT fk_preference_user FOREIGN KEY (user_id) + REFERENCES member_auth(user_id) + ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE KEY uq_user_food (user_id, food_keyword) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file From 1eeb9eeb647aa5a6e7813470e342001bed48df05 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 22:01:48 +0900 Subject: [PATCH 007/120] =?UTF-8?q?fix(MemeberAuth):=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=EB=AA=85=20=EB=A7=A4=ED=95=91=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/{Member.java => MemberAuth.java} | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) rename src/main/java/com/stcom/smartmealtable/domain/member/{Member.java => MemberAuth.java} (84%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java similarity index 84% rename from src/main/java/com/stcom/smartmealtable/domain/member/Member.java rename to src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java index a0c1b69..2d5ee4e 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java @@ -1,19 +1,19 @@ package com.stcom.smartmealtable.domain.member; import com.stcom.smartmealtable.common.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import jakarta.persistence.Table; import jakarta.validation.constraints.Email; @Entity -@Table(name = "MEMBER_AUTH") -public class Member extends BaseEntity { +public class MemberAuth extends BaseEntity { @Id @GeneratedValue + @Column(name = "member_id") private Long id; @Email @@ -35,4 +35,8 @@ public void changePassword(String rawOldPassword, String rawNewPassword) public boolean isMatchedPassword(final String rawPassword) throws PasswordFailedExceededException { return password.isMatched(rawPassword); } + + public boolean isEmailVerified() { + return isEmailVerified; + } } From 3ef64c2fcf8bfab9be95890a350543071beccc65 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 22:07:17 +0900 Subject: [PATCH 008/120] =?UTF-8?q?feat(MemberProfile):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberAuth.java | 4 +++ .../domain/member/MemberProfile.java | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java index 2d5ee4e..2fbfa9d 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java @@ -6,6 +6,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; import jakarta.validation.constraints.Email; @Entity @@ -24,6 +25,9 @@ public class MemberAuth extends BaseEntity { private boolean isEmailVerified; + @OneToOne(mappedBy = "memberAuth") + private MemberProfile memberProfile; + public void changePassword(String rawOldPassword, String rawNewPassword) throws PasswordFailedExceededException, PasswordPolicyException { if (rawOldPassword.isBlank() || rawNewPassword.isBlank()) { diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java new file mode 100644 index 0000000..915a4b9 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -0,0 +1,25 @@ +package com.stcom.smartmealtable.domain.member; + +import com.stcom.smartmealtable.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; + +@Entity +public class MemberProfile extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "member_profile_id") + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberAuth memberAuth; + + private String username; +} From 13631a0fadd23957f1830f208aa7c467dbeeda83 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 22:14:07 +0900 Subject: [PATCH 009/120] =?UTF-8?q?feat(socialAccount)=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EA=B3=84=EC=A0=95=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/SocialAccount.java | 34 +++++++++++++++++++ .../domain/member/SocialLoginProvider.java | 5 +++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java new file mode 100644 index 0000000..ca9217e --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java @@ -0,0 +1,34 @@ +package com.stcom.smartmealtable.domain.member; + +import com.stcom.smartmealtable.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; + +@Entity +public class SocialAccount extends BaseEntity { + + @Id + @GeneratedValue + @Column(name = "social_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberAuth memberAuth; + + private SocialLoginProvider provider; + + private String providerUserId; + + private String accessToken; + + private String refreshToken; + + private LocalDateTime tokenExpiresAt; +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java new file mode 100644 index 0000000..1d80587 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.member; + +public enum SocialLoginProvider { + GOOGLE, KAKAO +} From 3d8d90a4cdf8597664c9e94b75b84ec7f239edb7 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 3 May 2025 22:14:34 +0900 Subject: [PATCH 010/120] =?UTF-8?q?fix(-):=20BaseEntity=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/stcom/smartmealtable/common/BaseEntity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java index 1f10be8..1ec6a4e 100644 --- a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java +++ b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java @@ -14,9 +14,9 @@ public abstract class BaseEntity { @CreatedDate - private LocalDateTime createdDate; + private LocalDateTime createdDateTime; @LastModifiedDate - private LocalDateTime modifiedDate; + private LocalDateTime modifiedDateTime; } \ No newline at end of file From 07650b8312e85b713d6daa55dc5c6a28498cf6fa Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sat, 10 May 2025 23:30:13 +0900 Subject: [PATCH 011/120] =?UTF-8?q?refactor:=20BaseEntity=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=81=EC=86=8D=20=EA=B4=80=EA=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/common/BaseEntity.java | 18 +++++++-------- .../smartmealtable/common/BaseTimeEntity.java | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java index 1ec6a4e..ec4116e 100644 --- a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java +++ b/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java @@ -2,21 +2,19 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; import lombok.Getter; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public abstract class BaseEntity { +public class BaseEntity extends BaseTimeEntity { - @CreatedDate - private LocalDateTime createdDateTime; + @CreatedBy + private String createdBy; - @LastModifiedDate - private LocalDateTime modifiedDateTime; - -} \ No newline at end of file + @LastModifiedBy + private String lastModifiedBy; +} diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java b/src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java new file mode 100644 index 0000000..04b022c --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java @@ -0,0 +1,22 @@ +package com.stcom.smartmealtable.common; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime lastModifiedDate; + +} \ No newline at end of file From e83267e8b741f9bca225207fa8681651093ba50e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:26:12 +0900 Subject: [PATCH 012/120] =?UTF-8?q?feat(Address):=20address=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/common/Address.java | 46 +++++++++++++++++++ .../domain/common/AddressType.java | 5 ++ 2 files changed, 51 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/common/Address.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java new file mode 100644 index 0000000..49b7eca --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java @@ -0,0 +1,46 @@ +package com.stcom.smartmealtable.domain.common; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import java.math.BigDecimal; +import lombok.Getter; + +@Entity +@Getter +public class Address { + + @Id + @GeneratedValue + private Long id; + + private String lotNumberAddress; + + private String roadAddress; + + private String detailAddress; + + private String alias; + + private BigDecimal latitude; + + private BigDecimal longitude; + + @Enumerated(EnumType.STRING) + private AddressType type; + + public void updateAddress(String lotNumberAddress, String roadAddress, String detailAddress, + BigDecimal latitude, BigDecimal longitude) { + this.lotNumberAddress = lotNumberAddress; + this.roadAddress = roadAddress; + this.detailAddress = detailAddress; + this.latitude = latitude; + this.longitude = longitude; + } + + public void changeAddressType(AddressType newType) { + this.type = newType; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java b/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java new file mode 100644 index 0000000..a7006f1 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.common; + +public enum AddressType { + +} From 25c83251a17c6d2b7c2c6af4a254711b4ee4947f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:27:00 +0900 Subject: [PATCH 013/120] =?UTF-8?q?fix:=20=EC=83=9D=EC=84=B1=EC=9D=BC/?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=9D=BC=20Auditing=20=EC=95=A0=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/SmartmealtableApplication.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/SmartmealtableApplication.java b/src/main/java/com/stcom/smartmealtable/SmartmealtableApplication.java index 7e68ba0..1173f6d 100644 --- a/src/main/java/com/stcom/smartmealtable/SmartmealtableApplication.java +++ b/src/main/java/com/stcom/smartmealtable/SmartmealtableApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SmartmealtableApplication { - public static void main(String[] args) { - SpringApplication.run(SmartmealtableApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SmartmealtableApplication.class, args); + } } From 7f3c0a4892fa22b9d952d7fc21231cecc8736251 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:27:32 +0900 Subject: [PATCH 014/120] =?UTF-8?q?refactor(password):=20=EB=B9=8C?= =?UTF-8?q?=EB=8D=94=20=ED=8C=A8=ED=84=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/stcom/smartmealtable/domain/member/MemberPassword.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java index 07f3ba7..1fbfc74 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -6,7 +6,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; -import lombok.Builder; import lombok.NoArgsConstructor; @Embeddable @@ -24,7 +23,6 @@ public class MemberPassword { private long ttl; - @Builder public MemberPassword(String rawPassword) throws PasswordPolicyException { checkPasswordPolicy(rawPassword); this.password_hash = encodePassword(rawPassword); From 1c6f6fd98ac41ac85d6735085f06c9c804eaf4ed Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:27:53 +0900 Subject: [PATCH 015/120] =?UTF-8?q?feat(SocialAccount):=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/SocialAccount.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java index ca9217e..a1dfbd6 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java @@ -1,17 +1,22 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.common.BaseEntity; +import com.stcom.smartmealtable.common.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; +import lombok.Builder; +import lombok.NoArgsConstructor; @Entity -public class SocialAccount extends BaseEntity { +@NoArgsConstructor +public class SocialAccount extends BaseTimeEntity { @Id @GeneratedValue @@ -22,6 +27,7 @@ public class SocialAccount extends BaseEntity { @JoinColumn(name = "member_id") private MemberAuth memberAuth; + @Enumerated(EnumType.STRING) private SocialLoginProvider provider; private String providerUserId; @@ -31,4 +37,21 @@ public class SocialAccount extends BaseEntity { private String refreshToken; private LocalDateTime tokenExpiresAt; + + @Builder + public SocialAccount(MemberAuth memberAuth, SocialLoginProvider provider, String providerUserId, + String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { + this.memberAuth = memberAuth; + this.provider = provider; + this.providerUserId = providerUserId; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.tokenExpiresAt = tokenExpiresAt; + } + + public void updateToken(String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.tokenExpiresAt = tokenExpiresAt; + } } From 2fa7fac52ea7cae02e7e6587ec4a394702a2db88 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:28:06 +0900 Subject: [PATCH 016/120] =?UTF-8?q?feat(MemberProfile):=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberProfile.java | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 915a4b9..0906afc 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -1,25 +1,51 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.common.BaseEntity; +import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.common.Address; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import java.util.ArrayList; +import java.util.List; @Entity -public class MemberProfile extends BaseEntity { +public class MemberProfile extends BaseTimeEntity { @Id @GeneratedValue @Column(name = "member_profile_id") private Long id; - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @OneToOne(mappedBy = "memberProfile") private MemberAuth memberAuth; - private String username; + private String fullName; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "member_id") + private List
addressHistory = new ArrayList<>(); + + protected void linkMemberAuth(MemberAuth memberAuth) { + this.memberAuth = memberAuth; + } + + public void changeFullName(String newName) { + if (newName.isBlank()) { + throw new IllegalArgumentException("이름은 비어 있을 수 없습니다."); + } + this.fullName = newName; + } + + public void addAddress(Address address) { + addressHistory.add(address); + } + + public void removeAddress(Address address) { + addressHistory.remove(address); + } } From 54b244294e0c6a9c9f8efefb29713c0309896c25 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 01:28:20 +0900 Subject: [PATCH 017/120] =?UTF-8?q?feat(MemberAuth):=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberAuth.java | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java index 2fbfa9d..262cc0b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java @@ -1,16 +1,21 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.common.BaseEntity; +import com.stcom.smartmealtable.common.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.validation.constraints.Email; +import lombok.Builder; +import lombok.NoArgsConstructor; @Entity -public class MemberAuth extends BaseEntity { +@NoArgsConstructor +public class MemberAuth extends BaseTimeEntity { @Id @GeneratedValue @@ -23,11 +28,24 @@ public class MemberAuth extends BaseEntity { @Embedded private MemberPassword password; - private boolean isEmailVerified; + // TODO: 이메일 인증 기능 구현해야함 + private boolean isEmailVerified = true; - @OneToOne(mappedBy = "memberAuth") + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_profile_id") private MemberProfile memberProfile; + @Builder + public MemberAuth(String email, String rawPassword) throws PasswordPolicyException { + this.email = email; + this.password = new MemberPassword(rawPassword); + } + + public void registerMemberProfile(MemberProfile profile) { + memberProfile = profile; + profile.linkMemberAuth(this); + } + public void changePassword(String rawOldPassword, String rawNewPassword) throws PasswordFailedExceededException, PasswordPolicyException { if (rawOldPassword.isBlank() || rawNewPassword.isBlank()) { @@ -43,4 +61,10 @@ public boolean isMatchedPassword(final String rawPassword) throws PasswordFailed public boolean isEmailVerified() { return isEmailVerified; } + + public void verifyEmail() { + this.isEmailVerified = true; + } + + } From b747ac6e8daa50a7b2424678994db4be45bf72e4 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 15:57:44 +0900 Subject: [PATCH 018/120] =?UTF-8?q?feat(budget):=20=EC=98=88=EC=82=B0=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/Budget/Budget.java | 56 +++++++++++++++++++ .../domain/Budget/DailyBudget.java | 12 ++++ .../domain/Budget/MonthlyBudget.java | 12 ++++ 3 files changed, 80 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java new file mode 100644 index 0000000..54e3823 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -0,0 +1,56 @@ +package com.stcom.smartmealtable.domain.Budget; + +import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.member.MemberAuth; +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn +@NoArgsConstructor +public abstract class Budget extends BaseTimeEntity { + + @Id + @GeneratedValue + @Column(name = "budget_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private MemberAuth memberAuth; + + private BigDecimal spendAmount = BigDecimal.ZERO; + + private BigDecimal limit; + + private BigDecimal availableAmount; + + protected Budget(MemberAuth memberAuth) { + this.memberAuth = memberAuth; + } + + public void addSpent(BigDecimal amount) { + this.spendAmount = spendAmount.add(amount); + } + + public void resetSpent() { + this.spendAmount = BigDecimal.ZERO; + } + + public boolean isOverLimit() { + return spendAmount.compareTo(limit) > 0; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java new file mode 100644 index 0000000..ce19a59 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -0,0 +1,12 @@ +package com.stcom.smartmealtable.domain.Budget; + +import jakarta.persistence.Entity; +import java.time.LocalDate; +import lombok.Getter; + +@Entity +@Getter +public class DailyBudget extends Budget { + + private LocalDate localDate; +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java new file mode 100644 index 0000000..0e3d696 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -0,0 +1,12 @@ +package com.stcom.smartmealtable.domain.Budget; + +import jakarta.persistence.Entity; +import java.time.YearMonth; +import lombok.Getter; + +@Entity +@Getter +public class MonthlyBudget extends Budget { + + private YearMonth yearMonth; +} From 60d28c6ed2b324029a8b228343e84ddd60bb9730 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 15:58:11 +0900 Subject: [PATCH 019/120] =?UTF-8?q?fix:=20PK=20=EC=B9=BC=EB=9F=BC=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stcom/smartmealtable/domain/common/Address.java | 2 ++ .../com/stcom/smartmealtable/domain/member/SocialAccount.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java index 49b7eca..4124e4c 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java +++ b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java @@ -1,5 +1,6 @@ package com.stcom.smartmealtable.domain.common; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -14,6 +15,7 @@ public class Address { @Id @GeneratedValue + @Column(name = "address_id") private Long id; private String lotNumberAddress; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java index a1dfbd6..5e67cd6 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java @@ -20,7 +20,7 @@ public class SocialAccount extends BaseTimeEntity { @Id @GeneratedValue - @Column(name = "social_id") + @Column(name = "social_account_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) From e51a39d6b36e06956e4d77b3748c7af8824410b9 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 16:27:24 +0900 Subject: [PATCH 020/120] =?UTF-8?q?feat:=20AddressType=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/stcom/smartmealtable/domain/common/AddressType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java b/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java index a7006f1..a83f75f 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java @@ -1,5 +1,5 @@ package com.stcom.smartmealtable.domain.common; public enum AddressType { - + HOME, SCHOOL, OFFICE } From 80cf73b18f8ddf9bcc17a03bda293afa8671ba3a Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 16:28:09 +0900 Subject: [PATCH 021/120] =?UTF-8?q?refactor:=20password=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberPasswordTest.java | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java index 0a69693..d8e0df2 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java @@ -13,10 +13,14 @@ class MemberPasswordTest { void 비밀번호_정책_성공() throws Exception { String successRawPassword = "abcdefg123"; assertDoesNotThrow(() -> - MemberPassword.builder().rawPassword(successRawPassword).build() + createPassword(successRawPassword) ); } + private MemberPassword createPassword(String rawPassword) throws PasswordPolicyException { + return new MemberPassword(rawPassword); + } + @Test void 비밀번호_정책_8자이상() throws Exception { String failedRawPassword = "abc123"; @@ -26,7 +30,7 @@ class MemberPasswordTest { private void checkFailedCase(String failedRawPassword) { assertThrows(PasswordPolicyException.class, () -> - MemberPassword.builder().rawPassword(failedRawPassword).build() + createPassword(failedRawPassword) ); } @@ -52,19 +56,14 @@ private void checkFailedCase(String failedRawPassword) { @Test void 비밀번호_일치() throws Exception { // given - MemberPassword newPassword = MemberPassword.builder() - .rawPassword("abcdefg1234") - .build(); + MemberPassword newPassword = createPassword("abcdefg1234"); // then - assertThat(newPassword.isMatched("abcdefg1234")); + assertThat(newPassword.isMatched("abcdefg1234")).isTrue(); } @Test void 비밀번호_변경_실패_이전비밀번호_불일치() throws Exception { - MemberPassword oldPassword - = MemberPassword.builder() - .rawPassword("abcdefg1234") - .build(); + MemberPassword oldPassword = createPassword("abcdefg1234"); assertThrows(PasswordFailedExceededException.class, () -> oldPassword.changePassword("abcdccc1234", "abcdefg123")); // 실패해야 함.) @@ -72,10 +71,7 @@ private void checkFailedCase(String failedRawPassword) { @Test void 비밀번호_변경_실패_새비밀번호_정책실패() throws Exception { - MemberPassword oldPassword - = MemberPassword.builder() - .rawPassword("abcdefg1234") - .build(); + MemberPassword oldPassword = createPassword("abcdefg1234"); assertThrows(PasswordPolicyException.class, () -> oldPassword.changePassword("abcde", "abcdefg1234")); // 실패해야 함.) @@ -83,10 +79,7 @@ private void checkFailedCase(String failedRawPassword) { @Test void 비밀번호_변경_실패_새비밀번호_기존과동일() throws Exception { - MemberPassword oldPassword - = MemberPassword.builder() - .rawPassword("abcdefg1234") - .build(); + MemberPassword oldPassword = createPassword("abcdefg1234"); assertThrows(PasswordPolicyException.class, () -> oldPassword.changePassword("abcdefg1234", "abcdefg1234")); // 실패해야 함.) @@ -95,10 +88,7 @@ private void checkFailedCase(String failedRawPassword) { @Test void 비밀번호_변경_성공() throws Exception { - MemberPassword password - = MemberPassword.builder() - .rawPassword("abcdefg1234") - .build(); + MemberPassword password = new MemberPassword("abcdefg1234"); assertDoesNotThrow( () -> password.changePassword("aaabcd1234", "abcdefg1234")); // 실패해야 함.) From cbb62d5781adb070fcb2d64cb6e59193fd07f0ea Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 16:28:48 +0900 Subject: [PATCH 022/120] =?UTF-8?q?refactor(budget):=20budget=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/Budget/Budget.java | 17 ++++++++++++++--- .../domain/Budget/DailyBudget.java | 12 +++++++++++- .../domain/Budget/MonthlyBudget.java | 10 ++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index 54e3823..13404ad 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -36,20 +36,31 @@ public abstract class Budget extends BaseTimeEntity { private BigDecimal limit; - private BigDecimal availableAmount; - - protected Budget(MemberAuth memberAuth) { + protected Budget(MemberAuth memberAuth, BigDecimal limit) { this.memberAuth = memberAuth; + this.limit = limit; } public void addSpent(BigDecimal amount) { this.spendAmount = spendAmount.add(amount); } + public void addSpent(int amount) { + this.spendAmount = spendAmount.add(BigDecimal.valueOf(amount)); + } + + public void addSpent(double amount) { + this.spendAmount = spendAmount.add(BigDecimal.valueOf(amount)); + } + public void resetSpent() { this.spendAmount = BigDecimal.ZERO; } + public BigDecimal getAvailableAmount() { + return limit.subtract(spendAmount); + } + public boolean isOverLimit() { return spendAmount.compareTo(limit) > 0; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java index ce19a59..3f78f99 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -1,12 +1,22 @@ package com.stcom.smartmealtable.domain.Budget; +import com.stcom.smartmealtable.domain.member.MemberAuth; import jakarta.persistence.Entity; +import java.math.BigDecimal; import java.time.LocalDate; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@NoArgsConstructor public class DailyBudget extends Budget { - private LocalDate localDate; + public DailyBudget(MemberAuth memberAuth, BigDecimal limit, + LocalDate date) { + super(memberAuth, limit); + this.date = date; + } + + private LocalDate date; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java index 0e3d696..0c17a97 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -1,12 +1,22 @@ package com.stcom.smartmealtable.domain.Budget; +import com.stcom.smartmealtable.domain.member.MemberAuth; import jakarta.persistence.Entity; +import java.math.BigDecimal; import java.time.YearMonth; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@NoArgsConstructor public class MonthlyBudget extends Budget { + public MonthlyBudget(MemberAuth memberAuth, BigDecimal limit, + YearMonth yearMonth) { + super(memberAuth, limit); + this.yearMonth = yearMonth; + } + private YearMonth yearMonth; } From 21ee09de21eea2242d36a465160ceb17af507cff Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 16:29:13 +0900 Subject: [PATCH 023/120] =?UTF-8?q?test(budget):=20=EC=98=88=EC=82=B0=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/Budget/BudgetTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java new file mode 100644 index 0000000..a6dae50 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java @@ -0,0 +1,73 @@ +package com.stcom.smartmealtable.domain.Budget; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.MemberAuth; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import org.junit.jupiter.api.Test; + +class BudgetTest { + + @Test + void 예산_생성() throws Exception { + + // given + MemberAuth memberAuth = getMemberAuth(); + // when + DailyBudget budget1 = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); + MonthlyBudget budget2 = new MonthlyBudget(memberAuth, BigDecimal.valueOf(100000), YearMonth.now()); + // then + assertThat(budget1.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); + assertThat(budget1.getDate()).isNotNull(); + + assertThat(budget2.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); + } + + private MemberAuth getMemberAuth() { + return new MemberAuth(); + } + + @Test + void 예산_소비_정수() throws Exception { + // given + MemberAuth memberAuth = getMemberAuth(); + Budget budget = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); + // when + budget.addSpent(1000); + // then + assertThat(budget.getSpendAmount()).isEqualTo(BigDecimal.valueOf(1000)); + assertThat(budget.getAvailableAmount()).isEqualTo(BigDecimal.valueOf(99000)); + } + + @Test + void 예산_소비_소수() throws Exception { + // given + MemberAuth memberAuth = getMemberAuth(); + Budget budget = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); + // when + budget.addSpent(9999.9); + // then + assertThat(budget.getSpendAmount()).isEqualTo(BigDecimal.valueOf(9999.9)); + assertThat(budget.getAvailableAmount()).isEqualTo(BigDecimal.valueOf(90000.1)); + } + + @Test + void 예산_초과_유무() throws Exception { + // given + MemberAuth memberAuth1 = getMemberAuth(); + MemberAuth memberAuth2 = getMemberAuth(); + Budget budget1 = new DailyBudget(memberAuth1, BigDecimal.valueOf(100000), LocalDate.now()); + Budget budget2 = new DailyBudget(memberAuth2, BigDecimal.valueOf(100000), LocalDate.now()); + + // when + budget1.addSpent(99000); + budget2.addSpent(110000); + + // then + assertThat(budget1.isOverLimit()).isFalse(); + assertThat(budget2.isOverLimit()).isTrue(); + + } +} \ No newline at end of file From 75fd6642529ca51de6a26d6ecfd06b6a7f33a314 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 11 May 2025 23:45:09 +0900 Subject: [PATCH 024/120] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/Budget/Budget.java | 8 +++--- .../domain/Budget/DailyBudget.java | 6 ++-- .../domain/Budget/MonthlyBudget.java | 8 +++--- .../member/{MemberAuth.java => Member.java} | 6 ++-- .../domain/member/MemberProfile.java | 6 ++-- .../domain/member/SocialAccount.java | 6 ++-- .../domain/Budget/BudgetTest.java | 28 +++++++++---------- 7 files changed, 35 insertions(+), 33 deletions(-) rename src/main/java/com/stcom/smartmealtable/domain/member/{MemberAuth.java => Member.java} (90%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index 13404ad..001a279 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -1,7 +1,7 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.common.BaseTimeEntity; -import com.stcom.smartmealtable.domain.member.MemberAuth; +import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Entity; @@ -30,14 +30,14 @@ public abstract class Budget extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") - private MemberAuth memberAuth; + private Member member; private BigDecimal spendAmount = BigDecimal.ZERO; private BigDecimal limit; - protected Budget(MemberAuth memberAuth, BigDecimal limit) { - this.memberAuth = memberAuth; + protected Budget(Member member, BigDecimal limit) { + this.member = member; this.limit = limit; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java index 3f78f99..744f409 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.Budget; -import com.stcom.smartmealtable.domain.member.MemberAuth; +import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.LocalDate; @@ -12,9 +12,9 @@ @NoArgsConstructor public class DailyBudget extends Budget { - public DailyBudget(MemberAuth memberAuth, BigDecimal limit, + public DailyBudget(Member member, BigDecimal limit, LocalDate date) { - super(memberAuth, limit); + super(member, limit); this.date = date; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java index 0c17a97..1f4b950 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.Budget; -import com.stcom.smartmealtable.domain.member.MemberAuth; +import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.YearMonth; @@ -12,11 +12,11 @@ @NoArgsConstructor public class MonthlyBudget extends Budget { - public MonthlyBudget(MemberAuth memberAuth, BigDecimal limit, + public MonthlyBudget(Member member, BigDecimal limit, YearMonth yearMonth) { - super(memberAuth, limit); + super(member, limit); this.yearMonth = yearMonth; } - + private YearMonth yearMonth; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java similarity index 90% rename from src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java rename to src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 262cc0b..5eb763f 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberAuth.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -9,13 +9,15 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import lombok.Builder; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor -public class MemberAuth extends BaseTimeEntity { +@Table(name = "member_auth") +public class Member extends BaseTimeEntity { @Id @GeneratedValue @@ -36,7 +38,7 @@ public class MemberAuth extends BaseTimeEntity { private MemberProfile memberProfile; @Builder - public MemberAuth(String email, String rawPassword) throws PasswordPolicyException { + public Member(String email, String rawPassword) throws PasswordPolicyException { this.email = email; this.password = new MemberPassword(rawPassword); } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 0906afc..6a36602 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -22,7 +22,7 @@ public class MemberProfile extends BaseTimeEntity { private Long id; @OneToOne(mappedBy = "memberProfile") - private MemberAuth memberAuth; + private Member member; private String fullName; @@ -30,8 +30,8 @@ public class MemberProfile extends BaseTimeEntity { @JoinColumn(name = "member_id") private List
addressHistory = new ArrayList<>(); - protected void linkMemberAuth(MemberAuth memberAuth) { - this.memberAuth = memberAuth; + protected void linkMemberAuth(Member member) { + this.member = member; } public void changeFullName(String newName) { diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java index 5e67cd6..38650a8 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java @@ -25,7 +25,7 @@ public class SocialAccount extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") - private MemberAuth memberAuth; + private Member member; @Enumerated(EnumType.STRING) private SocialLoginProvider provider; @@ -39,9 +39,9 @@ public class SocialAccount extends BaseTimeEntity { private LocalDateTime tokenExpiresAt; @Builder - public SocialAccount(MemberAuth memberAuth, SocialLoginProvider provider, String providerUserId, + public SocialAccount(Member member, SocialLoginProvider provider, String providerUserId, String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { - this.memberAuth = memberAuth; + this.member = member; this.provider = provider; this.providerUserId = providerUserId; this.accessToken = accessToken; diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java index a6dae50..5b87a4a 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.stcom.smartmealtable.domain.member.MemberAuth; +import com.stcom.smartmealtable.domain.member.Member; import java.math.BigDecimal; import java.time.LocalDate; import java.time.YearMonth; @@ -14,10 +14,10 @@ class BudgetTest { void 예산_생성() throws Exception { // given - MemberAuth memberAuth = getMemberAuth(); + Member member = getMember(); // when - DailyBudget budget1 = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); - MonthlyBudget budget2 = new MonthlyBudget(memberAuth, BigDecimal.valueOf(100000), YearMonth.now()); + DailyBudget budget1 = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); + MonthlyBudget budget2 = new MonthlyBudget(member, BigDecimal.valueOf(100000), YearMonth.now()); // then assertThat(budget1.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); assertThat(budget1.getDate()).isNotNull(); @@ -25,15 +25,15 @@ class BudgetTest { assertThat(budget2.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); } - private MemberAuth getMemberAuth() { - return new MemberAuth(); + private Member getMember() { + return new Member(); } @Test void 예산_소비_정수() throws Exception { // given - MemberAuth memberAuth = getMemberAuth(); - Budget budget = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); + Member member = getMember(); + Budget budget = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); // when budget.addSpent(1000); // then @@ -44,8 +44,8 @@ private MemberAuth getMemberAuth() { @Test void 예산_소비_소수() throws Exception { // given - MemberAuth memberAuth = getMemberAuth(); - Budget budget = new DailyBudget(memberAuth, BigDecimal.valueOf(100000), LocalDate.now()); + Member member = getMember(); + Budget budget = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); // when budget.addSpent(9999.9); // then @@ -56,10 +56,10 @@ private MemberAuth getMemberAuth() { @Test void 예산_초과_유무() throws Exception { // given - MemberAuth memberAuth1 = getMemberAuth(); - MemberAuth memberAuth2 = getMemberAuth(); - Budget budget1 = new DailyBudget(memberAuth1, BigDecimal.valueOf(100000), LocalDate.now()); - Budget budget2 = new DailyBudget(memberAuth2, BigDecimal.valueOf(100000), LocalDate.now()); + Member member1 = getMember(); + Member member2 = getMember(); + Budget budget1 = new DailyBudget(member1, BigDecimal.valueOf(100000), LocalDate.now()); + Budget budget2 = new DailyBudget(member2, BigDecimal.valueOf(100000), LocalDate.now()); // when budget1.addSpent(99000); From e60e97839d80603acd33efe98ad033b0146cb9b1 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 12 May 2025 23:37:37 +0900 Subject: [PATCH 025/120] =?UTF-8?q?feat:=20OAuth=20=EC=8A=A4=ED=8E=99=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/web/dto/ApiResponse.java | 13 +++ .../web/dto/token/JwtTokenResponseDto.java | 15 +++ .../web/dto/token/TokenDto.java | 28 +++++ .../smartmealtable/web/social/Kakao.java | 107 ++++++++++++++++++ .../smartmealtable/web/social/Social.java | 15 +++ .../web/social/SocialManager.java | 27 +++++ 6 files changed, 205 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/social/Kakao.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/social/Social.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java new file mode 100644 index 0000000..00b2e32 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java @@ -0,0 +1,13 @@ +package com.stcom.smartmealtable.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ApiResponse { + + private String status; + private String message; + private T data; +} diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java new file mode 100644 index 0000000..0df6175 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java @@ -0,0 +1,15 @@ +package com.stcom.smartmealtable.web.dto.token; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class JwtTokenResponseDto { + + private String accessToken; + private String refreshToken; + private int expiresIn; + private String tokenType; + private boolean isNewUser; +} diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java b/src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java new file mode 100644 index 0000000..2d6156a --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java @@ -0,0 +1,28 @@ +package com.stcom.smartmealtable.web.dto.token; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class TokenDto { + + private String accessToken; + private String refreshToken; + private Integer expiresIn; + private String tokenType; + private String provider; + private String providerUserId; + + @Builder + public TokenDto(String accessToken, String refreshToken, Integer expiresIn, String tokenType, String provider, + String providerUserId) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.tokenType = tokenType; + this.provider = provider; + this.providerUserId = providerUserId; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/social/Kakao.java b/src/main/java/com/stcom/smartmealtable/web/social/Kakao.java new file mode 100644 index 0000000..8c0b78c --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/social/Kakao.java @@ -0,0 +1,107 @@ +package com.stcom.smartmealtable.web.social; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.stcom.smartmealtable.web.dto.token.TokenDto; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.ResponseSpec; + +public class Kakao implements Social { + + @Override + public RequestBodySpec getRequestMessage(RestClient client, String code) { + return client.post() + .uri(uriBuilder -> uriBuilder.path("https://kauth.kakao.com/oauth/token").build()) + .headers(httpHeaders -> httpHeaders.setContentType(MediaType.APPLICATION_JSON)) + .body(new KakaoTokenRequest(code)); + } + + @Override + public TokenDto getTokenResponse(ResponseSpec responseSpec) { + KakaoTokenResponse tokenResponse = responseSpec.body(KakaoTokenResponse.class); + return TokenDto.builder() + .accessToken(tokenResponse.getAccessToken()) + .refreshToken(tokenResponse.getRefreshToken()) + .expiresIn(tokenResponse.getExpiresIn()) + .tokenType(tokenResponse.getTokenType()) + .provider("Kakao") + .providerUserId(extractProviderUserId(tokenResponse.getIdToken())) + .build(); + + } + + @Override + public String extractProviderUserId(String idToken) { + if (idToken == null || idToken.isEmpty()) { + return null; + } + + try { + String[] jwtParts = idToken.split("\\."); + if (jwtParts.length != 3) { + return null; + } + + String payload = new String(java.util.Base64.getUrlDecoder().decode(jwtParts[1])); + + // Jackson ObjectMapper를 사용하여 JSON 파싱 + ObjectMapper mapper = new ObjectMapper(); + JsonNode payloadJson = mapper.readTree(payload); + + // 'sub' 필드가 사용자 ID임 + return payloadJson.has("sub") ? payloadJson.get("sub").asText() : null; + } catch (Exception e) { + // 예외 발생 시 로깅 및 null 반환 + System.err.println("카카오 ID 토큰 파싱 오류: " + e.getMessage()); + return null; + } + } + + @Data + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class KakaoTokenRequest { + + public KakaoTokenRequest(String code) { + this.code = code; + } + + @NotBlank + private String grantType = "authorization_code"; + + @NotBlank + @Value("${kakao.oauth.client-id}") + private String clientId; + + @NotBlank + @Value("${kakao.oauth.redirect-uri}") + private String redirectUri; + + @NotBlank + private String code; + } + + @Data + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class KakaoTokenResponse { + + private String tokenType; + + private String accessToken; + + private String idToken; + + private Integer expiresIn; + + private String refreshToken; + + private Integer refreshTokenExpiresIn; + + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/social/Social.java b/src/main/java/com/stcom/smartmealtable/web/social/Social.java new file mode 100644 index 0000000..f38c3b1 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/social/Social.java @@ -0,0 +1,15 @@ +package com.stcom.smartmealtable.web.social; + +import com.stcom.smartmealtable.web.dto.token.TokenDto; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.ResponseSpec; + +public interface Social { + + RequestBodySpec getRequestMessage(RestClient client, String code); + + TokenDto getTokenResponse(ResponseSpec responseSpec); + + String extractProviderUserId(String idToken); +} diff --git a/src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java b/src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java new file mode 100644 index 0000000..c415d3c --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java @@ -0,0 +1,27 @@ +package com.stcom.smartmealtable.web.social; + +import com.stcom.smartmealtable.web.dto.token.TokenDto; +import java.util.HashMap; +import java.util.Map; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.ResponseSpec; + +@Component +public class SocialManager { + + private final Map socialMap = new HashMap<>(); + + public SocialManager() { + socialMap.put("Kakao", new Kakao()); + } + + public RequestBodySpec getTokenRequestMessage(RestClient client, String provider, String code) { + return socialMap.get(provider).getRequestMessage(client, code); + } + + public TokenDto getTokenResponse(ResponseSpec responseSpec, String provider) { + return socialMap.get(provider).getTokenResponse(responseSpec); + } +} From 201cc958d6b9fa8ff11d569925281562ac05de1e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 12 May 2025 23:38:00 +0900 Subject: [PATCH 026/120] =?UTF-8?q?refactor:=20provider=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/SocialLoginProvider.java | 5 ----- .../domain/{member => social}/SocialAccount.java | 10 ++++------ .../domain/social/SocialAccountRepository.java | 9 +++++++++ 3 files changed, 13 insertions(+), 11 deletions(-) delete mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java rename src/main/java/com/stcom/smartmealtable/domain/{member => social}/SocialAccount.java (82%) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java b/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java deleted file mode 100644 index 1d80587..0000000 --- a/src/main/java/com/stcom/smartmealtable/domain/member/SocialLoginProvider.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.stcom.smartmealtable.domain.member; - -public enum SocialLoginProvider { - GOOGLE, KAKAO -} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java similarity index 82% rename from src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java rename to src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index 38650a8..ab9d5e5 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -1,10 +1,9 @@ -package com.stcom.smartmealtable.domain.member; +package com.stcom.smartmealtable.domain.social; import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -27,8 +26,7 @@ public class SocialAccount extends BaseTimeEntity { @JoinColumn(name = "member_id") private Member member; - @Enumerated(EnumType.STRING) - private SocialLoginProvider provider; + private String provider; private String providerUserId; @@ -39,7 +37,7 @@ public class SocialAccount extends BaseTimeEntity { private LocalDateTime tokenExpiresAt; @Builder - public SocialAccount(Member member, SocialLoginProvider provider, String providerUserId, + public SocialAccount(Member member, String provider, String providerUserId, String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { this.member = member; this.provider = provider; diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java new file mode 100644 index 0000000..13837e3 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java @@ -0,0 +1,9 @@ +package com.stcom.smartmealtable.domain.social; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SocialAccountRepository extends JpaRepository { + + Optional findByProviderAndProviderUserId(String provider, String providerUserId); +} From 4c02e6da9ef398ca406eca27da10588cbe0d9b80 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 12 May 2025 23:38:59 +0900 Subject: [PATCH 027/120] =?UTF-8?q?feat:=20jwt=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberRepository.java | 7 +++ .../domain/member/MemberService.java | 49 +++++++++++++++++++ .../domain/social/SocialAccountService.java | 27 ++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java new file mode 100644 index 0000000..1699f5d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.domain.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java new file mode 100644 index 0000000..a98ba3d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java @@ -0,0 +1,49 @@ +package com.stcom.smartmealtable.domain.member; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + @Value("${jwt.secret}") + private String jwtSecret; + + // Access 토큰 생성 (만료 시간: 1시간) + public String createAccessToken(String memberId) { + return createToken(memberId, 1000 * 60 * 60); + } + + // Refresh 토큰 생성 (만료 시간: 2주) + public String createRefreshToken(String memberId) { + return createToken(memberId, 1000 * 60 * 60 * 24 * 14); + } + + // JWT 토큰 생성 공통 메서드 + private String createToken(String memberId, long expireTime) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + expireTime); + + Map claims = new HashMap<>(); + claims.put("memberId", memberId); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) + .compact(); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java new file mode 100644 index 0000000..42f78d6 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java @@ -0,0 +1,27 @@ +package com.stcom.smartmealtable.domain.social; + +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SocialAccountService { + + private final SocialAccountRepository socialAccountRepository; + + public SocialAccount getSocialAccount(String provider, String providerUserId) { + return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId) + .orElse(null); + } + + @Transactional + public void updateToken(SocialAccount socialAccount, String accessToken, String refreshToken, + LocalDateTime tokenExpiresAt) { + socialAccount.updateToken(accessToken, refreshToken, tokenExpiresAt); + socialAccountRepository.save(socialAccount); + } + +} From b3055b4506c9a567440414573e1660accb8e5787 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:43:10 +0900 Subject: [PATCH 028/120] =?UTF-8?q?chore:=20jwt=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/build.gradle b/build.gradle index 4e792a1..ad11723 100644 --- a/build.gradle +++ b/build.gradle @@ -1,44 +1,46 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.4.4' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.stcom' version = '0.0.1-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-batch' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.batch:spring-batch-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-batch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.batch:spring-batch-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } From 28435d9d259688eaea7c0bae738193192d285d80 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:43:28 +0900 Subject: [PATCH 029/120] =?UTF-8?q?feat(food):=20food=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/food/FoodCategory.java | 5 ++ .../domain/food/FoodPreference.java | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java new file mode 100644 index 0000000..8ed328e --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.food; + +public enum FoodCategory { + KOREAN, JAPANESE, WESTERN, CHINESE +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java new file mode 100644 index 0000000..6fb68fd --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java @@ -0,0 +1,54 @@ +package com.stcom.smartmealtable.domain.food; + +import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.member.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class FoodPreference extends BaseTimeEntity { + + @Id + @GeneratedValue + @Column(name = "food_preference_id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Enumerated(EnumType.STRING) + @Column(name = "category") + private FoodCategory category; + + private boolean isPreferred; + + private Double weight; + + @Builder + public FoodPreference(Member member, FoodCategory category, boolean isPreferred, Double weight) { + this.member = member; + this.category = category; + this.isPreferred = isPreferred; + this.weight = weight; + } + + + public void updatePreference(boolean isPreferred, Double weight) { + this.isPreferred = isPreferred; + this.weight = weight; + } + +} From 31d2e4f69bbdfe8d8b4ee42555a7204728600c01 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:43:56 +0900 Subject: [PATCH 030/120] =?UTF-8?q?fix(member):=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/stcom/smartmealtable/domain/member/Member.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 5eb763f..102e355 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -12,11 +12,13 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor @Table(name = "member_auth") +@Getter public class Member extends BaseTimeEntity { @Id @@ -30,6 +32,8 @@ public class Member extends BaseTimeEntity { @Embedded private MemberPassword password; + private String fullName; + // TODO: 이메일 인증 기능 구현해야함 private boolean isEmailVerified = true; @@ -38,7 +42,8 @@ public class Member extends BaseTimeEntity { private MemberProfile memberProfile; @Builder - public Member(String email, String rawPassword) throws PasswordPolicyException { + public Member(String fullName, String email, String rawPassword) throws PasswordPolicyException { + this.fullName = fullName; this.email = email; this.password = new MemberPassword(rawPassword); } From b5f7cc566db211791e03058b25c6c20cff9cc139 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:44:20 +0900 Subject: [PATCH 031/120] =?UTF-8?q?fix(memberProfile):=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/MemberGroup.java | 5 +++++ .../domain/member/MemberProfile.java | 15 +++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java new file mode 100644 index 0000000..75f1486 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.member; + +public enum MemberGroup { + UNIVERSITY, STUDENT, NONE, ADMIN +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 6a36602..f42ba1b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -5,6 +5,8 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; @@ -24,7 +26,12 @@ public class MemberProfile extends BaseTimeEntity { @OneToOne(mappedBy = "memberProfile") private Member member; - private String fullName; + @Enumerated(EnumType.STRING) + private MemberGroup memberGroup; + + private Long groupCode; + + private String nickName; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "member_id") @@ -34,11 +41,11 @@ protected void linkMemberAuth(Member member) { this.member = member; } - public void changeFullName(String newName) { - if (newName.isBlank()) { + public void changeNickName(String newNickName) { + if (newNickName.isBlank()) { throw new IllegalArgumentException("이름은 비어 있을 수 없습니다."); } - this.fullName = newName; + this.nickName = newNickName; } public void addAddress(Address address) { From 26534ccd2a4aa72711d704de53c14651e3ecdd54 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:45:25 +0900 Subject: [PATCH 032/120] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberService.java | 47 ++----- .../security/JwtTokenService.java | 119 ++++++++++++++++++ 2 files changed, 130 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java index a98ba3d..6a838f4 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java @@ -1,49 +1,24 @@ package com.stcom.smartmealtable.domain.member; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; - - @Value("${jwt.secret}") - private String jwtSecret; - - // Access 토큰 생성 (만료 시간: 1시간) - public String createAccessToken(String memberId) { - return createToken(memberId, 1000 * 60 * 60); - } - - // Refresh 토큰 생성 (만료 시간: 2주) - public String createRefreshToken(String memberId) { - return createToken(memberId, 1000 * 60 * 60 * 24 * 14); + + public boolean isEmailExists(String email) { + List findMembers = memberRepository.findMemberByEmail(email); + if (findMembers.isEmpty()) { + return false; + } + return true; } - - // JWT 토큰 생성 공통 메서드 - private String createToken(String memberId, long expireTime) { - Date now = new Date(); - Date expiration = new Date(now.getTime() + expireTime); - - Map claims = new HashMap<>(); - claims.put("memberId", memberId); - - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiration) - .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) - .compact(); + + public void saveMember(Member member) { + memberRepository.save(member); } } diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java new file mode 100644 index 0000000..f1f841e --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java @@ -0,0 +1,119 @@ +package com.stcom.smartmealtable.security; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberRepository; +import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JwtTokenService { + + private final MemberRepository memberRepository; + + @Value("${jwt.secret}") + private String jwtSecret; + + public String createAccessToken(Long memberId) { + return createToken(String.valueOf(memberId), 1000 * 60 * 60); + } + + public String createAccessToken(String memberId) { + return createToken(memberId, 1000 * 60 * 60); + } + + public String createRefreshToken(Long memberId) { + return createToken(String.valueOf(memberId), 1000 * 60 * 60 * 24 * 14); + } + + private String createToken(String memberId, long expireTime) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + expireTime); + + Map claims = new HashMap<>(); + claims.put("memberId", memberId); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) + .compact(); + } + + public JwtTokenResponseDto createTokenDto(Long memberId) { + return new JwtTokenResponseDto( + createAccessToken(memberId), + createRefreshToken(memberId), + 3600, + "Bearar" + ); + } + + public String extractMemberIdFromRefreshToken(String refreshToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(refreshToken) + .getBody() + .get("memberId", String.class); + } catch (Exception e) { + throw new RuntimeException("유효하지 않은 리프레시 토큰입니다"); + } + } + + public boolean validateToken(String token) { + try { + // Bearer 접두사 제거 + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + // 토큰 검증 + Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token); + + return true; + } catch (Exception e) { + return false; + } + } + + public Member getClaim(String token) { + try { + // Bearer 접두사 제거 + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + String memberIdStr = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token) + .getBody() + .get("memberId", String.class); + + Long memberId = Long.parseLong(memberIdStr); + + // 회원 정보 조회 + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다")); + + return member; + } catch (Exception e) { + throw new RuntimeException("토큰 처리 중 오류가 발생했습니다: " + e.getMessage()); + } + } +} From 108d3a21c61ad7fa032ef16eefb31f089df77863 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:45:58 +0900 Subject: [PATCH 033/120] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/domain/member/MemberRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java index 1699f5d..5fca764 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java @@ -1,7 +1,10 @@ package com.stcom.smartmealtable.domain.member; +import jakarta.validation.constraints.Email; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + List findMemberByEmail(@Email String email); } From 1f31793bd899743d9d2f5e29bca033326bb680b3 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:46:18 +0900 Subject: [PATCH 034/120] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/domain/social/SocialAccount.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index ab9d5e5..dd38293 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -30,6 +30,8 @@ public class SocialAccount extends BaseTimeEntity { private String providerUserId; + private String tokenType; + private String accessToken; private String refreshToken; @@ -37,11 +39,12 @@ public class SocialAccount extends BaseTimeEntity { private LocalDateTime tokenExpiresAt; @Builder - public SocialAccount(Member member, String provider, String providerUserId, + public SocialAccount(Member member, String provider, String providerUserId, String tokenType, String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { this.member = member; this.provider = provider; this.providerUserId = providerUserId; + this.tokenType = tokenType; this.accessToken = accessToken; this.refreshToken = refreshToken; this.tokenExpiresAt = tokenExpiresAt; From bdab1417ea7d950f6ecbf8bf301c011e3750102c Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:47:01 +0900 Subject: [PATCH 035/120] =?UTF-8?q?feat(socialAccount):=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EA=B3=84=EC=A0=95=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../social/SocialAccountRepository.java | 8 +++++ .../domain/social/SocialAccountService.java | 34 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java index 13837e3..4352cc0 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java @@ -2,8 +2,16 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface SocialAccountRepository extends JpaRepository { Optional findByProviderAndProviderUserId(String provider, String providerUserId); + + @Query("select sa.member.id from SocialAccount sa where sa.provider = :provider and sa.providerUserId = :providerUserId") + Optional findMemberIdByProviderAndProviderUserId( + @Param("provider") String provider, + @Param("providerUserId") String providerUserId + ); } diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java index 42f78d6..c685350 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java @@ -1,5 +1,8 @@ package com.stcom.smartmealtable.domain.social; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberRepository; +import com.stcom.smartmealtable.web.dto.token.TokenDto; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -11,10 +14,31 @@ public class SocialAccountService { private final SocialAccountRepository socialAccountRepository; + private final MemberRepository memberRepository; - public SocialAccount getSocialAccount(String provider, String providerUserId) { - return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId) - .orElse(null); + @Transactional + public void createNewAccount(TokenDto tokenDto) { + Member member = new Member(); + memberRepository.save(member); + + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider(tokenDto.getProvider()) + .providerUserId(tokenDto.getProviderUserId()) + .tokenType(tokenDto.getTokenType()) + .accessToken(tokenDto.getAccessToken()) + .refreshToken(tokenDto.getRefreshToken()) + .tokenExpiresAt(LocalDateTime.now().plusSeconds(tokenDto.getExpiresIn())) + .build(); + socialAccountRepository.save(socialAccount); + } + + public SocialAccount findSocialAccount(String provider, String providerUserId) { + return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).orElse(null); + } + + public boolean isNewUser(String provider, String providerUserId) { + return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).isPresent(); } @Transactional @@ -24,4 +48,8 @@ public void updateToken(SocialAccount socialAccount, String accessToken, String socialAccountRepository.save(socialAccount); } + public Long findMemberId(String provider, String providerUserId) { + return socialAccountRepository.findMemberIdByProviderAndProviderUserId(provider, providerUserId) + .orElseThrow(() -> new IllegalStateException("회원 정보가 없습니다. 먼저 회원 정보를 생성해주세요")); + } } From d3de818bf7efc846b3d41eef1616fcf67eb45f84 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:47:22 +0900 Subject: [PATCH 036/120] =?UTF-8?q?refactor:=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Kakao.java => auth/social/KakaoHttpMessage.java} | 4 ++-- .../Social.java => auth/social/SocialHttpMessage.java} | 4 ++-- .../smartmealtable/web/{ => auth}/social/SocialManager.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/main/java/com/stcom/smartmealtable/web/{social/Kakao.java => auth/social/KakaoHttpMessage.java} (96%) rename src/main/java/com/stcom/smartmealtable/web/{social/Social.java => auth/social/SocialHttpMessage.java} (82%) rename src/main/java/com/stcom/smartmealtable/web/{ => auth}/social/SocialManager.java (80%) diff --git a/src/main/java/com/stcom/smartmealtable/web/social/Kakao.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java similarity index 96% rename from src/main/java/com/stcom/smartmealtable/web/social/Kakao.java rename to src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java index 8c0b78c..be099a6 100644 --- a/src/main/java/com/stcom/smartmealtable/web/social/Kakao.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.web.social; +package com.stcom.smartmealtable.web.auth.social; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -13,7 +13,7 @@ import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; -public class Kakao implements Social { +public class KakaoHttpMessage implements SocialHttpMessage { @Override public RequestBodySpec getRequestMessage(RestClient client, String code) { diff --git a/src/main/java/com/stcom/smartmealtable/web/social/Social.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java similarity index 82% rename from src/main/java/com/stcom/smartmealtable/web/social/Social.java rename to src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java index f38c3b1..57fe22f 100644 --- a/src/main/java/com/stcom/smartmealtable/web/social/Social.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java @@ -1,11 +1,11 @@ -package com.stcom.smartmealtable.web.social; +package com.stcom.smartmealtable.web.auth.social; import com.stcom.smartmealtable.web.dto.token.TokenDto; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; -public interface Social { +public interface SocialHttpMessage { RequestBodySpec getRequestMessage(RestClient client, String code); diff --git a/src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java similarity index 80% rename from src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java rename to src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java index c415d3c..78a08a7 100644 --- a/src/main/java/com/stcom/smartmealtable/web/social/SocialManager.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.web.social; +package com.stcom.smartmealtable.web.auth.social; import com.stcom.smartmealtable.web.dto.token.TokenDto; import java.util.HashMap; @@ -11,10 +11,10 @@ @Component public class SocialManager { - private final Map socialMap = new HashMap<>(); + private final Map socialMap = new HashMap<>(); public SocialManager() { - socialMap.put("Kakao", new Kakao()); + socialMap.put("Kakao", new KakaoHttpMessage()); } public RequestBodySpec getTokenRequestMessage(RestClient client, String provider, String code) { From 46e4b2f401ef08243489ee975c129774b91b02d2 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:47:56 +0900 Subject: [PATCH 037/120] =?UTF-8?q?feat:=20jwt=20=ED=97=A4=EB=8D=94=20argu?= =?UTF-8?q?ment=20resovler=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthorization.java | 15 ++++++ .../stcom/smartmealtable/web/WebConfig.java | 20 ++++++++ .../JwtAuthorizationArgumentResolver.java | 46 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/WebConfig.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java b/src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java new file mode 100644 index 0000000..45a4508 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java @@ -0,0 +1,15 @@ +package com.stcom.smartmealtable.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JwtAuthorization { + + boolean required() default true; +} diff --git a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java new file mode 100644 index 0000000..87ae710 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java @@ -0,0 +1,20 @@ +package com.stcom.smartmealtable.web; + +import com.stcom.smartmealtable.web.argumentresolver.JwtAuthorizationArgumentResolver; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(jwtAuthorizationArgumentResolver); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java new file mode 100644 index 0000000..ef9e7ce --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java @@ -0,0 +1,46 @@ +package com.stcom.smartmealtable.web.argumentresolver; + +import com.stcom.smartmealtable.security.JwtAuthorization; +import com.stcom.smartmealtable.security.JwtTokenService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class JwtAuthorizationArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtTokenService jwtTokenService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(JwtAuthorization.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + + // 헤더 값 체크 + if (httpServletRequest != null) { + String token = httpServletRequest.getHeader("Authorization"); + + if (token != null && !token.trim().equals("")) { + // 토큰 있을 경우 검증 + if (jwtTokenService.validateToken(token)) { + // 검증 후 MemberProfile 리턴 + return jwtTokenService.getClaim(token); + } + } + } + + // 토큰 값이 없으면 에러 + throw new RuntimeException("권한 없음."); + } +} From 38d27c7ec397399cb2dc03254fd9c95fd3e13ea7 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:48:13 +0900 Subject: [PATCH 038/120] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/web/dto/ApiResponse.java | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java index 00b2e32..5b3527e 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java +++ b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java @@ -1,13 +1,54 @@ package com.stcom.smartmealtable.web.dto; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; @Data -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class ApiResponse { + private static final String SUCCESS_STATUS = "SUCCESS"; + private static final String FAIL_STATUS = "FAIL"; + private static final String ERROR_STATUS = "ERROR"; + private String status; private String message; private T data; + + public static ApiResponse createSuccess(T data) { + return new ApiResponse<>(SUCCESS_STATUS, null, data); + } + + public static ApiResponse createSuccessWithNoContent() { + return new ApiResponse<>(SUCCESS_STATUS, null, null); + } + + // Hibernate Validator에 의해 유효하지 않은 데이터로 인해 API 호출이 거부될때 반환 + public static ApiResponse createFail(BindingResult bindingResult) { + Map errors = new HashMap<>(); + + List allErrors = bindingResult.getAllErrors(); + for (ObjectError error : allErrors) { + if (error instanceof FieldError) { + errors.put(((FieldError) error).getField(), error.getDefaultMessage()); + } else { + errors.put(error.getObjectName(), error.getDefaultMessage()); + } + } + return new ApiResponse<>(FAIL_STATUS, null, errors); + } + + // 예외 발생으로 API 호출 실패시 반환 + public static ApiResponse createError(String message) { + return new ApiResponse<>(ERROR_STATUS, message, null); + } + + } From 5fafcafaaa56011390e7e77f894a1b0dede6a880 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:48:39 +0900 Subject: [PATCH 039/120] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20Dto=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/dto/token/JwtTokenResponseDto.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java index 0df6175..7be6553 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java +++ b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java @@ -1,10 +1,8 @@ package com.stcom.smartmealtable.web.dto.token; -import lombok.AllArgsConstructor; import lombok.Data; @Data -@AllArgsConstructor public class JwtTokenResponseDto { private String accessToken; @@ -12,4 +10,11 @@ public class JwtTokenResponseDto { private int expiresIn; private String tokenType; private boolean isNewUser; + + public JwtTokenResponseDto(String accessToken, String refreshToken, int expiresIn, String tokenType) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresIn = expiresIn; + this.tokenType = tokenType; + } } From 1ac7e46a000a577dc16703e655b6f612d2d12d0b Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:49:00 +0900 Subject: [PATCH 040/120] =?UTF-8?q?feat(Auth):=20=EC=9D=B8=EC=A6=9D=20api?= =?UTF-8?q?=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/auth/OAuth2Controller.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java new file mode 100644 index 0000000..168bc25 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java @@ -0,0 +1,86 @@ +package com.stcom.smartmealtable.web.auth; + +import com.stcom.smartmealtable.domain.social.SocialAccountService; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.web.auth.social.SocialManager; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; +import com.stcom.smartmealtable.web.dto.token.TokenDto; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; + +@RestController +@Slf4j +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class OAuth2Controller { + + private final SocialManager socialManager; + private final RestClient client = RestClient.create(); + private final SocialAccountService socialAccountService; + private final JwtTokenService jwtTokenService; + + @PostMapping("/oauth2/code") + public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { + RequestBodySpec tokenRequestMessage = socialManager.getTokenRequestMessage(client, request.getProvider(), + request.getAuthorizationCode()); + TokenDto token = socialManager.getTokenResponse(tokenRequestMessage.retrieve(), request.getProvider()); + boolean isNewUser = socialAccountService.isNewUser(token.getProvider(), + token.getProviderUserId()); + if (!isNewUser) { + socialAccountService.createNewAccount(token); + } + + JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto( + socialAccountService.findMemberId(token.getProvider(), token.getProviderUserId())); + tokenDto.setNewUser(isNewUser); + return ApiResponse.createSuccess(tokenDto); + } + + @PostMapping("/api/v1/auth/token/refresh") + public ApiResponse refreshAccessToken(@RequestBody JwtRefreshTokenRequest request) { + String memberId = jwtTokenService.extractMemberIdFromRefreshToken(request.getRefreshToken()); + String accessToken = jwtTokenService.createAccessToken(memberId); + + return ApiResponse.createSuccess( + new JwtRefreshedAccessTokenDto(accessToken, 3600, "Bearar") + ); + } + + + @Data + @AllArgsConstructor + static class JwtTokenRequest { + + @NotEmpty + private String provider; + + @NotEmpty + private String authorizationCode; + } + + @Data + @AllArgsConstructor + static class JwtRefreshedAccessTokenDto { + private String accessToken; + private int expiresIn; + private String tokenType; + } + + @Data + static class JwtRefreshTokenRequest { + + @NotEmpty + private String refreshToken; + } + +} From fcc1b4c54879d1bc010a99c8cce4123cc96d0d60 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 19:49:23 +0900 Subject: [PATCH 041/120] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5,=20=ED=9A=8C=EC=9B=90=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/member/MemberController.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/web/member/MemberController.java diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java new file mode 100644 index 0000000..2553d44 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java @@ -0,0 +1,113 @@ +package com.stcom.smartmealtable.web.member; + +import com.stcom.smartmealtable.domain.common.Address; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberGroup; +import com.stcom.smartmealtable.domain.member.MemberService; +import com.stcom.smartmealtable.domain.member.PasswordPolicyException; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +public class MemberController { + + private final MemberService memberService; + private final JwtTokenService jwtTokenService; + + @GetMapping("/members/email/check") + public ResponseEntity> checkEmail(@Email @RequestParam String email, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ApiResponse.createFail(bindingResult)); + } + if (memberService.isEmailExists(email)) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("이미 존재하는 이메일입니다.")); + } + return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); + } + + @PostMapping() + public ResponseEntity> createMember(@Valid @RequestBody CreateMemberRequest request, + BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ApiResponse.createFail(bindingResult)); + } + + if (memberService.isEmailExists(request.getEmail())) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("이미 존재하는 이메일입니다.")); + } + + if (!request.getPassword().equals(request.getConfirmPassword())) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("비밀번호가 일치하지 않습니다.")); + } + + Member member; + try { + member = Member.builder() + .fullName(request.getFullName()) + .email(request.getEmail()) + .rawPassword(request.getPassword()) + .build(); + } catch (PasswordPolicyException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError(e.getMessage())); + } + memberService.saveMember(member); + JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId()); + tokenDto.setNewUser(true); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.createSuccess(tokenDto)); + } + +// @PostMapping("/profile") +// public ApiResponse createMemberProfile(@JwtAuthorization Member member, +// @RequestBody CreateMemberProfileRequest request) { +// +// } + + @Data + @AllArgsConstructor + static class CreateMemberRequest { + + @Email + private String email; + private String password; + private String confirmPassword; + private String fullName; + } + + @Data + @AllArgsConstructor + static class CreateMemberProfileRequest { + private MemberGroup groupType; + private Long groupCode; + private Address homeAddress; + private Map foodPreference; + private List hateFoods; + private Long dailyLimitAmount; + private Long monthlyLimitAmount; + } + + +} From d1fdd0265918c34d461fd2b8c1b7d5c778a90f64 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 20:25:15 +0900 Subject: [PATCH 042/120] =?UTF-8?q?fix:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/Member.java | 2 - table.ddl | 126 +++++++++--------- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 102e355..1e404fa 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -9,7 +9,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; -import jakarta.persistence.Table; import jakarta.validation.constraints.Email; import lombok.Builder; import lombok.Getter; @@ -17,7 +16,6 @@ @Entity @NoArgsConstructor -@Table(name = "member_auth") @Getter public class Member extends BaseTimeEntity { diff --git a/table.ddl b/table.ddl index ea144ad..37c9b1b 100644 --- a/table.ddl +++ b/table.ddl @@ -1,35 +1,35 @@ -- 1. 회원 관련 테이블 --- 1.1. 회원 인증 테이블 (MemberAuth) -CREATE TABLE IF NOT EXISTS member_auth ( - user_id BIGINT NOT NULL AUTO_INCREMENT, - email VARCHAR(255) DEFAULT NULL, +-- 1.1. 회원 인증 테이블 (Member) +CREATE TABLE IF NOT EXISTS member ( + member_id BIGINT NOT NULL AUTO_INCREMENT, + email VARCHAR(255) DEFAULT NULL, password_hash VARCHAR(255) DEFAULT NULL, is_email_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (user_id), + PRIMARY KEY (member_id), UNIQUE KEY uq_email (email) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 1.2. 회원 프로필 테이블 (MemberProfile) CREATE TABLE IF NOT EXISTS member_profile ( profile_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, full_name VARCHAR(255) NOT NULL, default_image VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (profile_id), - CONSTRAINT fk_memberprofile_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_memberprofile_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 1.3. 소셜 로그인 테이블 (SocialLogin) CREATE TABLE IF NOT EXISTS social_login ( social_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, provider VARCHAR(50) NOT NULL, provider_user_id VARCHAR(255) NOT NULL, access_token VARCHAR(512) NOT NULL, @@ -38,8 +38,8 @@ CREATE TABLE IF NOT EXISTS social_login ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (social_id), - CONSTRAINT fk_sociallogin_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_sociallogin_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY uq_provider_user (provider, provider_user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -47,19 +47,19 @@ CREATE TABLE IF NOT EXISTS social_login ( -- 1.4. 회원 주소 테이블 (MemberAddress) CREATE TABLE IF NOT EXISTS member_address ( address_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - address VARCHAR(255) NOT NULL, -- 기본 주소 - road_address VARCHAR(255) NOT NULL, -- 도로명 주소 - detail_address VARCHAR(255), -- 상세 주소 - alias VARCHAR(255), -- 주소 별칭 - latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 - longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 - status VARCHAR(255) NOT NULL, -- 주소 상태 (HOME, COMPANY, ETC, N:삭제) + member_id BIGINT NOT NULL, + address VARCHAR(255) NOT NULL, -- 기본 주소 + road_address VARCHAR(255) NOT NULL, -- 도로명 주소 + detail_address VARCHAR(255), -- 상세 주소 + alias VARCHAR(255), -- 주소 별칭 + latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 + longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 + status VARCHAR(255) NOT NULL, -- 주소 상태 (HOME, COMPANY, ETC, N:삭제) created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (address_id), - CONSTRAINT fk_memberaddress_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_memberaddress_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -69,30 +69,30 @@ CREATE TABLE IF NOT EXISTS member_address ( -- 2.1. 월별 예산 테이블 (MonthlyBudget) CREATE TABLE IF NOT EXISTS monthly_budget ( budget_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - year_month CHAR(7) NOT NULL, -- "YYYY-MM" 형식 (예: "2025-04") + member_id BIGINT NOT NULL, + year_month CHAR(7) NOT NULL, -- "YYYY-MM" 형식 (예: "2025-04") monthly_limit DECIMAL(10,2) NOT NULL, -- 한 달 목표 예산 (예: 500000.00) spent_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 지금까지 소비한 금액 updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (budget_id), - UNIQUE KEY uq_user_yearmonth (user_id, year_month), - CONSTRAINT fk_monthlybudget_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + UNIQUE KEY uq_member_yearmonth (member_id, year_month), + CONSTRAINT fk_monthlybudget_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2.2. 일별 예산 테이블 (DailyBudget) CREATE TABLE IF NOT EXISTS daily_budget ( daily_budget_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, budget_date DATE NOT NULL, - daily_limit DECIMAL(10,2) NOT NULL, -- 일일 목표 예산 (예: 20000.00) + daily_limit DECIMAL(10,2) NOT NULL, -- 일일 목표 예산 (예: 20000.00) spent_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 하루 동안 소비한 총액 updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (daily_budget_id), - UNIQUE KEY uq_user_date (user_id, budget_date), - CONSTRAINT fk_dailybudget_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + UNIQUE KEY uq_member_date (member_id, budget_date), + CONSTRAINT fk_dailybudget_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -102,18 +102,18 @@ CREATE TABLE IF NOT EXISTS daily_budget ( -- 3.1. 음식점 테이블 (FoodStore) CREATE TABLE IF NOT EXISTS food_store ( food_store_id BIGINT NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, -- 음식점 이름 - store_type ENUM('RESTAURANT', 'SCHOOL_CAFETERIA', 'CONVENIENCE_STORE') NOT NULL, -- 음식점 타입 - address VARCHAR(255) NOT NULL, -- 음식점 주소 - latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 - longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 - phone VARCHAR(50) DEFAULT NULL, -- 음식점 전화번호 - open_time TIME DEFAULT NULL, -- 오픈 시간 - close_time TIME DEFAULT NULL, -- 마감 시간 - external_id VARCHAR(255) DEFAULT NULL, -- 외부 음식점 ID(KAKAO) + name VARCHAR(255) NOT NULL, -- 음식점 이름 + store_type ENUM('RESTAURANT', 'SCHOOL_CAFETERIA', 'CONVENIENCE_STORE') NOT NULL, + address VARCHAR(255) NOT NULL, -- 음식점 주소 + latitude DECIMAL(10,7) DEFAULT NULL, -- 위도 + longitude DECIMAL(10,7) DEFAULT NULL, -- 경도 + phone VARCHAR(50) DEFAULT NULL, -- 음식점 전화번호 + open_time TIME DEFAULT NULL, -- 오픈 시간 + close_time TIME DEFAULT NULL, -- 마감 시간 + external_id VARCHAR(255) DEFAULT NULL, -- 외부 음식점 ID(KAKAO) created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - status VARCHAR(1) NOT NULL, -- 음식점 상태 (Y:활성화, N:삭제) + status VARCHAR(1) NOT NULL, -- 음식점 상태 (Y:활성화, N:삭제) PRIMARY KEY (food_store_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -122,11 +122,11 @@ CREATE TABLE IF NOT EXISTS food ( food_id BIGINT NOT NULL AUTO_INCREMENT, food_store_id BIGINT NOT NULL, name VARCHAR(255) NOT NULL, - category VARCHAR(100) NOT NULL, -- 음식 종류 + category VARCHAR(100) NOT NULL, -- 음식 종류 price DECIMAL(10,2) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - status VARCHAR(1) NOT NULL, -- 음식 상태 (Y:활성화, N:삭제) + status VARCHAR(1) NOT NULL, -- 음식 상태 (Y:활성화, N:삭제) PRIMARY KEY (food_id), CONSTRAINT fk_food_foodstore FOREIGN KEY (food_store_id) REFERENCES food_store(food_store_id) @@ -139,19 +139,19 @@ CREATE TABLE IF NOT EXISTS food ( -- 4.1. 즐겨찾는 음식점 테이블 (FavoriteFoodStore) CREATE TABLE IF NOT EXISTS favorite_food_store ( favorite_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, food_store_id BIGINT NOT NULL, rank_order INT DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (favorite_id), - CONSTRAINT fk_favorite_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_favorite_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_favorite_foodstore FOREIGN KEY (food_store_id) REFERENCES food_store(food_store_id) ON DELETE CASCADE ON UPDATE CASCADE, - UNIQUE KEY uq_favorite (user_id, food_store_id) + UNIQUE KEY uq_favorite (member_id, food_store_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; @@ -160,7 +160,7 @@ CREATE TABLE IF NOT EXISTS favorite_food_store ( -- 5.1. 지출 내역 테이블 (Expenditure) CREATE TABLE IF NOT EXISTS expenditure ( expenditure_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, food_store_id BIGINT DEFAULT NULL, transaction_date DATETIME NOT NULL, amount DECIMAL(10,2) NOT NULL, @@ -169,22 +169,22 @@ CREATE TABLE IF NOT EXISTS expenditure ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (expenditure_id), - CONSTRAINT fk_expenditure_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_expenditure_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_expenditure_foodstore FOREIGN KEY (food_store_id) REFERENCES food_store(food_store_id) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- 5.2. 주문서 ExpenditureFood +-- 5.2. 주문서 테이블 (ExpenditureFood) CREATE TABLE IF NOT EXISTS expenditure_food ( expenditure_food_id BIGINT NOT NULL AUTO_INCREMENT, expenditure_id BIGINT NOT NULL, food_id BIGINT NOT NULL, - quantity INT NOT NULL DEFAULT 1, -- 주문한 개수 - unit_price DECIMAL(10,2) NOT NULL, -- 주문 당시 단위 가격 (Food 테이블의 가격과 동일할 수도 있으나, 주문 시점의 가격을 기록) - total_price DECIMAL(10,2) NOT NULL, -- quantity * unit_price 계산 값 (혹은 주문 당시 총액) + quantity INT NOT NULL DEFAULT 1, -- 주문한 개수 + unit_price DECIMAL(10,2) NOT NULL, -- 주문 당시 단위 가격 + total_price DECIMAL(10,2) NOT NULL, -- quantity * unit_price 계산 값 created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (expenditure_food_id), @@ -199,19 +199,19 @@ CREATE TABLE IF NOT EXISTS expenditure_food ( -- 6. 사용자 음식 취향 테이블 --- 6.1. 사용자 음식 취향 선호 테이블 (UserFoodPreference) -CREATE TABLE IF NOT EXISTS user_food_preference ( - preference_id BIGINT NOT NULL AUTO_INCREMENT, - user_id BIGINT NOT NULL, - food_keyword VARCHAR(100) NOT NULL, +-- 6.1. 사용자 음식 취향 선호 테이블 (MemberFoodPreference) +CREATE TABLE IF NOT EXISTS member_food_preference ( + preference_id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + food_keyword VARCHAR(100) NOT NULL, food_category VARCHAR(100) NOT NULL, preference_order INT NOT NULL, is_dislike BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (preference_id), - CONSTRAINT fk_preference_user FOREIGN KEY (user_id) - REFERENCES member_auth(user_id) + CONSTRAINT fk_preference_member FOREIGN KEY (member_id) + REFERENCES member(member_id) ON DELETE CASCADE ON UPDATE CASCADE, - UNIQUE KEY uq_user_food (user_id, food_keyword) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file + UNIQUE KEY uq_member_food (member_id, food_keyword) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; From 34485318381ba926a2359f07d059087673fd155e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 20:28:47 +0900 Subject: [PATCH 043/120] =?UTF-8?q?fix:=20api=20uri=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/web/member/MemberController.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java index 2553d44..90f5e74 100644 --- a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java @@ -35,12 +35,8 @@ public class MemberController { private final MemberService memberService; private final JwtTokenService jwtTokenService; - @GetMapping("/members/email/check") - public ResponseEntity> checkEmail(@Email @RequestParam String email, BindingResult bindingResult) { - if (bindingResult.hasErrors()) { - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .body(ApiResponse.createFail(bindingResult)); - } + @GetMapping("/email/check") + public ResponseEntity> checkEmail(@Email @RequestParam String email) { if (memberService.isEmailExists(email)) { return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("이미 존재하는 이메일입니다.")); } From e54b69dd6710474fbd5a31bcf62079db2b3d45ed Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:20:13 +0900 Subject: [PATCH 044/120] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/web/auth/social/SocialConst.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java new file mode 100644 index 0000000..f14cd6a --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java @@ -0,0 +1,6 @@ +package com.stcom.smartmealtable.web.auth.social; + +public abstract class SocialConst { + + public static final String KAKAO = "kakao"; +} From 4231d56b92cb3c01ed78acd7c80204f93a24ec9b Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:20:36 +0900 Subject: [PATCH 045/120] =?UTF-8?q?fix:=20=EC=83=81=ED=83=9C=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/social/SocialAccountService.java | 2 +- .../stcom/smartmealtable/web/auth/OAuth2Controller.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java index c685350..54392de 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java @@ -38,7 +38,7 @@ public SocialAccount findSocialAccount(String provider, String providerUserId) { } public boolean isNewUser(String provider, String providerUserId) { - return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).isPresent(); + return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).isEmpty(); } @Transactional diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java index 168bc25..4fc4179 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java @@ -1,5 +1,6 @@ package com.stcom.smartmealtable.web.auth; + import com.stcom.smartmealtable.domain.social.SocialAccountService; import com.stcom.smartmealtable.security.JwtTokenService; import com.stcom.smartmealtable.web.auth.social.SocialManager; @@ -31,18 +32,22 @@ public class OAuth2Controller { @PostMapping("/oauth2/code") public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { + log.info("request = {}", request); RequestBodySpec tokenRequestMessage = socialManager.getTokenRequestMessage(client, request.getProvider(), request.getAuthorizationCode()); TokenDto token = socialManager.getTokenResponse(tokenRequestMessage.retrieve(), request.getProvider()); + log.info("response 성공 = {}", token); boolean isNewUser = socialAccountService.isNewUser(token.getProvider(), token.getProviderUserId()); - if (!isNewUser) { + log.info("새로운 멤버인지 확인 = {}", isNewUser); + if (isNewUser) { socialAccountService.createNewAccount(token); } JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto( socialAccountService.findMemberId(token.getProvider(), token.getProviderUserId())); tokenDto.setNewUser(isNewUser); + log.info("response = {}", tokenDto); return ApiResponse.createSuccess(tokenDto); } From ed97fcdf8de6a665b03e9d33cd22890904e8c783 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:21:30 +0900 Subject: [PATCH 046/120] =?UTF-8?q?fix:=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 컴포넌트 등록 -> @Value 적용 2. UriBuilder 절대 경로 -> 메서드 체이닝 3. JSON으로 요청 -> Form으로 요청(스펙을 잘못봄) --- .../web/auth/social/KakaoHttpMessage.java | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java index be099a6..579e9d3 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java @@ -1,28 +1,57 @@ package com.stcom.smartmealtable.web.auth.social; +import static com.stcom.smartmealtable.web.auth.social.SocialConst.KAKAO; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.stcom.smartmealtable.web.dto.token.TokenDto; -import jakarta.validation.constraints.NotBlank; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; +@Component +@Slf4j public class KakaoHttpMessage implements SocialHttpMessage { + @Value("${kakao.oauth.client-id}") + private String clientId; + + @Value("${kakao.oauth.redirect-uri}") + private String redirectUri; + @Override public RequestBodySpec getRequestMessage(RestClient client, String code) { return client.post() - .uri(uriBuilder -> uriBuilder.path("https://kauth.kakao.com/oauth/token").build()) - .headers(httpHeaders -> httpHeaders.setContentType(MediaType.APPLICATION_JSON)) - .body(new KakaoTokenRequest(code)); + .uri(uriBuilder -> uriBuilder + .scheme("https") + .host("kauth.kakao.com") + .path("/oauth/token") + .build()) + // form data 로 보내려면 반드시 URL_ENCODED + .headers(h -> h.setContentType(MediaType.APPLICATION_FORM_URLENCODED)) + .body(createFormData(code)); + } + + private MultiValueMap createFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "authorization_code"); + formData.add("client_id", clientId); + formData.add("redirect_uri", redirectUri); + formData.add("code", code); + log.info("client Id = {}", clientId); + return formData; } + @Override public TokenDto getTokenResponse(ResponseSpec responseSpec) { KakaoTokenResponse tokenResponse = responseSpec.body(KakaoTokenResponse.class); @@ -31,10 +60,9 @@ public TokenDto getTokenResponse(ResponseSpec responseSpec) { .refreshToken(tokenResponse.getRefreshToken()) .expiresIn(tokenResponse.getExpiresIn()) .tokenType(tokenResponse.getTokenType()) - .provider("Kakao") + .provider(KAKAO) .providerUserId(extractProviderUserId(tokenResponse.getIdToken())) .build(); - } @Override @@ -64,28 +92,28 @@ public String extractProviderUserId(String idToken) { } } - @Data - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - static class KakaoTokenRequest { - - public KakaoTokenRequest(String code) { - this.code = code; - } - - @NotBlank - private String grantType = "authorization_code"; - - @NotBlank - @Value("${kakao.oauth.client-id}") - private String clientId; - - @NotBlank - @Value("${kakao.oauth.redirect-uri}") - private String redirectUri; - - @NotBlank - private String code; - } +// @Data +// @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +// static class KakaoTokenRequest { +// +// public KakaoTokenRequest(String code) { +// this.code = code; +// } +// +// @NotBlank +// private String grantType = "authorization_code"; +// +// @NotBlank +// @Value("${kakao.oauth.client-id}") +// private String clientId; +// +// @NotBlank +// @Value("${kakao.oauth.redirect-uri}") +// private String redirectUri; +// +// @NotBlank +// private String code; +// } @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) From 478f2ea454f67e6910b2d2e074d82e8000c1b9d8 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:21:43 +0900 Subject: [PATCH 047/120] =?UTF-8?q?chore:=20CORS=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stcom/smartmealtable/web/WebConfig.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java index 87ae710..bdf44c3 100644 --- a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java +++ b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -17,4 +18,13 @@ public class WebConfig implements WebMvcConfigurer { public void addArgumentResolvers(List resolvers) { resolvers.add(jwtAuthorizationArgumentResolver); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } } From 6196b610e5be843eb9b35683982b6930b82ab4df Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:21:59 +0900 Subject: [PATCH 048/120] =?UTF-8?q?test:=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java index 7be6553..88ac8ed 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java +++ b/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java @@ -1,8 +1,10 @@ package com.stcom.smartmealtable.web.dto.token; import lombok.Data; +import lombok.ToString; @Data +@ToString public class JwtTokenResponseDto { private String accessToken; From ed5432fe51778b514860f3895a168659b8bd280f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:22:18 +0900 Subject: [PATCH 049/120] =?UTF-8?q?fix:=20=EC=8A=A4=ED=94=84=EB=A7=81=20?= =?UTF-8?q?=EB=B9=88=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/auth/social/SocialManager.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java index 78a08a7..52341a3 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java @@ -1,21 +1,23 @@ package com.stcom.smartmealtable.web.auth.social; +import static com.stcom.smartmealtable.web.auth.social.SocialConst.KAKAO; + import com.stcom.smartmealtable.web.dto.token.TokenDto; +import jakarta.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; @Component +@RequiredArgsConstructor public class SocialManager { private final Map socialMap = new HashMap<>(); - - public SocialManager() { - socialMap.put("Kakao", new KakaoHttpMessage()); - } + private final KakaoHttpMessage kakaoHttpMessage; public RequestBodySpec getTokenRequestMessage(RestClient client, String provider, String code) { return socialMap.get(provider).getRequestMessage(client, code); @@ -24,4 +26,9 @@ public RequestBodySpec getTokenRequestMessage(RestClient client, String provider public TokenDto getTokenResponse(ResponseSpec responseSpec, String provider) { return socialMap.get(provider).getTokenResponse(responseSpec); } + + @PostConstruct + public void init() { + socialMap.put(KAKAO, kakaoHttpMessage); + } } From 6407e0fdc0bd1d35187349a7b46e33386826b85e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 13 May 2025 22:22:37 +0900 Subject: [PATCH 050/120] =?UTF-8?q?chore:=20jwt=20json=20=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ad11723..32ccde4 100644 --- a/build.gradle +++ b/build.gradle @@ -30,8 +30,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation("io.jsonwebtoken:jjwt-api:0.11.5") - runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화/역직렬화에 Jackson 사용 compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.mysql:mysql-connector-j' From d7e1b7a50ce712550c08b41de79f17e496af8512 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 14 May 2025 01:32:58 +0900 Subject: [PATCH 051/120] =?UTF-8?q?feat:=20Google=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/auth/OAuth2Controller.java | 4 +- .../web/auth/social/GoogleHttpMessage.java | 108 ++++++++++++++++++ .../web/auth/social/KakaoHttpMessage.java | 24 ---- .../web/auth/social/SocialConst.java | 2 + ...ger.java => SocialHttpMessageManager.java} | 5 +- 5 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java rename src/main/java/com/stcom/smartmealtable/web/auth/social/{SocialManager.java => SocialHttpMessageManager.java} (83%) diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java index 4fc4179..e9adbed 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java @@ -3,7 +3,7 @@ import com.stcom.smartmealtable.domain.social.SocialAccountService; import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.web.auth.social.SocialManager; +import com.stcom.smartmealtable.web.auth.social.SocialHttpMessageManager; import com.stcom.smartmealtable.web.dto.ApiResponse; import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; import com.stcom.smartmealtable.web.dto.token.TokenDto; @@ -25,7 +25,7 @@ @RequiredArgsConstructor public class OAuth2Controller { - private final SocialManager socialManager; + private final SocialHttpMessageManager socialManager; private final RestClient client = RestClient.create(); private final SocialAccountService socialAccountService; private final JwtTokenService jwtTokenService; diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java new file mode 100644 index 0000000..ca351cb --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java @@ -0,0 +1,108 @@ +package com.stcom.smartmealtable.web.auth.social; + +import static com.stcom.smartmealtable.web.auth.social.SocialConst.GOOGLE; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.stcom.smartmealtable.web.dto.token.TokenDto; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.RequestBodySpec; +import org.springframework.web.client.RestClient.ResponseSpec; + +@Component +@Slf4j +public class GoogleHttpMessage implements SocialHttpMessage { + + @Value("${google.oauth.client-id}") + private String clientId; + + @Value("${google.oauth.client-secret}") + private String clientSecret; + + @Value("${google.oauth.redirect-uri}") + private String redirectUri; + + + @Override + public RequestBodySpec getRequestMessage(RestClient client, String code) { + return client.post() + .uri(uriBuilder -> uriBuilder + .scheme("https") + .host("oauth2.googleapis.com") + .path("/token") + .build()) + // form data 로 보내려면 반드시 URL_ENCODED + .headers(h -> h.setContentType(MediaType.APPLICATION_FORM_URLENCODED)) + .body(createFormData(code)); + } + + private MultiValueMap createFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("grant_type", "authorization_code"); + formData.add("code", code); + formData.add("redirect_uri", redirectUri); + formData.add("client_id", clientId); + formData.add("client_secret", clientSecret); + log.info("client Id = {}", clientId); + return formData; + } + + @Override + public TokenDto getTokenResponse(ResponseSpec responseSpec) { + GoogleTokenResponse tokenResponse = responseSpec.body(GoogleTokenResponse.class); + return TokenDto.builder() + .accessToken(tokenResponse.getAccessToken()) + .refreshToken(tokenResponse.getRefreshToken()) + .expiresIn(tokenResponse.getExpiresIn()) + .tokenType(tokenResponse.getTokenType()) + .provider(GOOGLE) + .providerUserId(extractProviderUserId(tokenResponse.getIdToken())) + .build(); + } + + + @Override + public String extractProviderUserId(String idToken) { + if (idToken == null || idToken.isBlank()) { + return null; + } + try { + String[] jwtParts = idToken.split("\\."); + if (jwtParts.length != 3) { + return null; + } + + String payload = new String(Base64.getUrlDecoder().decode(jwtParts[1]), StandardCharsets.UTF_8); + JsonNode payloadJson = new ObjectMapper().readTree(payload); + return payloadJson.path("sub").asText(null); + + } catch (Exception e) { + // 로깅: 어떤 공급자(token issuer)에 대한 토큰인지 같이 찍어도 좋습니다. + log.error("ID 토큰 파싱 오류: {}", e.getMessage()); + return null; + } + } + + @Data + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class GoogleTokenResponse { + + private String accessToken; + private Integer expiresIn; + private String refreshToken; + private String scope; + private String tokenType; + private String idToken; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java index 579e9d3..c5c18ec 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java @@ -83,7 +83,6 @@ public String extractProviderUserId(String idToken) { ObjectMapper mapper = new ObjectMapper(); JsonNode payloadJson = mapper.readTree(payload); - // 'sub' 필드가 사용자 ID임 return payloadJson.has("sub") ? payloadJson.get("sub").asText() : null; } catch (Exception e) { // 예외 발생 시 로깅 및 null 반환 @@ -92,29 +91,6 @@ public String extractProviderUserId(String idToken) { } } -// @Data -// @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -// static class KakaoTokenRequest { -// -// public KakaoTokenRequest(String code) { -// this.code = code; -// } -// -// @NotBlank -// private String grantType = "authorization_code"; -// -// @NotBlank -// @Value("${kakao.oauth.client-id}") -// private String clientId; -// -// @NotBlank -// @Value("${kakao.oauth.redirect-uri}") -// private String redirectUri; -// -// @NotBlank -// private String code; -// } - @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) static class KakaoTokenResponse { diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java index f14cd6a..f8ad30d 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java @@ -3,4 +3,6 @@ public abstract class SocialConst { public static final String KAKAO = "kakao"; + + public static final String GOOGLE = "google"; } diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java similarity index 83% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java rename to src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java index 52341a3..fa1a061 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialManager.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java @@ -1,5 +1,6 @@ package com.stcom.smartmealtable.web.auth.social; +import static com.stcom.smartmealtable.web.auth.social.SocialConst.GOOGLE; import static com.stcom.smartmealtable.web.auth.social.SocialConst.KAKAO; import com.stcom.smartmealtable.web.dto.token.TokenDto; @@ -14,10 +15,11 @@ @Component @RequiredArgsConstructor -public class SocialManager { +public class SocialHttpMessageManager { private final Map socialMap = new HashMap<>(); private final KakaoHttpMessage kakaoHttpMessage; + private final GoogleHttpMessage googleHttpMessage; public RequestBodySpec getTokenRequestMessage(RestClient client, String provider, String code) { return socialMap.get(provider).getRequestMessage(client, code); @@ -30,5 +32,6 @@ public TokenDto getTokenResponse(ResponseSpec responseSpec, String provider) { @PostConstruct public void init() { socialMap.put(KAKAO, kakaoHttpMessage); + socialMap.put(GOOGLE, googleHttpMessage); } } From 83734cfcc4948c071ca68c309469485d52fe6fd5 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 22:54:20 +0900 Subject: [PATCH 052/120] =?UTF-8?q?fix:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20PK?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=A0=84=EB=9E=B5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stcom/smartmealtable/domain/common/Address.java | 3 ++- .../com/stcom/smartmealtable/domain/food/FoodPreference.java | 3 ++- .../java/com/stcom/smartmealtable/domain/member/Member.java | 3 ++- .../com/stcom/smartmealtable/domain/member/MemberProfile.java | 3 ++- .../com/stcom/smartmealtable/domain/social/SocialAccount.java | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java index 4124e4c..81db5ff 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java +++ b/src/main/java/com/stcom/smartmealtable/domain/common/Address.java @@ -5,6 +5,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import java.math.BigDecimal; import lombok.Getter; @@ -14,7 +15,7 @@ public class Address { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "address_id") private Long id; diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java index 6fb68fd..d7074ae 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java @@ -8,6 +8,7 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; @@ -21,7 +22,7 @@ public class FoodPreference extends BaseTimeEntity { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "food_preference_id") private Long id; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 1e404fa..93614d8 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -6,6 +6,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; @@ -20,7 +21,7 @@ public class Member extends BaseTimeEntity { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") private Long id; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index f42ba1b..c60c4b6 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -8,6 +8,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToMany; @@ -19,7 +20,7 @@ public class MemberProfile extends BaseTimeEntity { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_profile_id") private Long id; diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index dd38293..ed9e87e 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -6,6 +6,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; @@ -18,7 +19,7 @@ public class SocialAccount extends BaseTimeEntity { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "social_account_id") private Long id; From 9ed41daf6c1a56f4711a11abe93bce939202597e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 22:57:58 +0900 Subject: [PATCH 053/120] =?UTF-8?q?chore:=20docker=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 11 +++++++++++ build.gradle | 4 ++++ 2 files changed, 15 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c968a73 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:21-jdk + +LABEL authors="luna" + +ARG JAR_FILE=build/libs/*.jar + +COPY ${JAR_FILE} app.jar + +ENV SPRING_PROFILES_ACTIVE=dev + +ENTRYPOINT ["java","-jar", "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 32ccde4..fdb53aa 100644 --- a/build.gradle +++ b/build.gradle @@ -45,3 +45,7 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +jar { + enabled = false +} \ No newline at end of file From 63d13c3ae0c293aa3eaa7ccd642a77c0ac01124f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 22:57:58 +0900 Subject: [PATCH 054/120] =?UTF-8?q?chore:=20docker=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ Dockerfile | 11 +++++++++++ build.gradle | 4 ++++ 3 files changed, 18 insertions(+) create mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index 941ebc9..c847a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -318,4 +318,7 @@ gradle-app.setting # yml *.yml +# CI/CD 워크플로우 파일은 추적 +!.github/workflows/ci-cd.yml + # End of https://www.toptal.com/developers/gitignore/api/java,intellij,macos,gradle,git,maven,visualstudiocode,windows diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c968a73 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:21-jdk + +LABEL authors="luna" + +ARG JAR_FILE=build/libs/*.jar + +COPY ${JAR_FILE} app.jar + +ENV SPRING_PROFILES_ACTIVE=dev + +ENTRYPOINT ["java","-jar", "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 32ccde4..fdb53aa 100644 --- a/build.gradle +++ b/build.gradle @@ -45,3 +45,7 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +jar { + enabled = false +} \ No newline at end of file From d70977d33643faa78d8079f010261d1cafd74848 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:14:29 +0900 Subject: [PATCH 055/120] =?UTF-8?q?Revert=20"chore:=20docker=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 63d13c3ae0c293aa3eaa7ccd642a77c0ac01124f. --- .gitignore | 3 --- Dockerfile | 11 ----------- build.gradle | 4 ---- 3 files changed, 18 deletions(-) delete mode 100644 Dockerfile diff --git a/.gitignore b/.gitignore index c847a6d..941ebc9 100644 --- a/.gitignore +++ b/.gitignore @@ -318,7 +318,4 @@ gradle-app.setting # yml *.yml -# CI/CD 워크플로우 파일은 추적 -!.github/workflows/ci-cd.yml - # End of https://www.toptal.com/developers/gitignore/api/java,intellij,macos,gradle,git,maven,visualstudiocode,windows diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c968a73..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM openjdk:21-jdk - -LABEL authors="luna" - -ARG JAR_FILE=build/libs/*.jar - -COPY ${JAR_FILE} app.jar - -ENV SPRING_PROFILES_ACTIVE=dev - -ENTRYPOINT ["java","-jar", "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index fdb53aa..32ccde4 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,3 @@ dependencies { tasks.named('test') { useJUnitPlatform() } - -jar { - enabled = false -} \ No newline at end of file From 029e64b62c9ff90f391955475dbd51f771cf4458 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:17:26 +0900 Subject: [PATCH 056/120] =?UTF-8?q?chore:=20ci/cd=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 941ebc9..c847a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -318,4 +318,7 @@ gradle-app.setting # yml *.yml +# CI/CD 워크플로우 파일은 추적 +!.github/workflows/ci-cd.yml + # End of https://www.toptal.com/developers/gitignore/api/java,intellij,macos,gradle,git,maven,visualstudiocode,windows From 5e8fdf38ca5ae629f15784703a8f49dad3f99009 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:21:43 +0900 Subject: [PATCH 057/120] =?UTF-8?q?fix:=20=ED=95=84=EB=93=9C=EB=AA=85?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=9E=84=EC=8B=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stcom/smartmealtable/domain/Budget/Budget.java | 3 +-- .../com/stcom/smartmealtable/domain/Budget/DailyBudget.java | 3 +-- .../com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index 001a279..d194ead 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -4,7 +4,6 @@ import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; -import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -17,7 +16,7 @@ import lombok.NoArgsConstructor; @Getter -@Entity +//@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn @NoArgsConstructor diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java index 744f409..6ca90c4 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -1,13 +1,12 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.domain.member.Member; -import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.LocalDate; import lombok.Getter; import lombok.NoArgsConstructor; -@Entity +//@Entity @Getter @NoArgsConstructor public class DailyBudget extends Budget { diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java index 1f4b950..6fc98e8 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -1,13 +1,12 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.domain.member.Member; -import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.YearMonth; import lombok.Getter; import lombok.NoArgsConstructor; -@Entity +//@Entity @Getter @NoArgsConstructor public class MonthlyBudget extends Budget { From f54ed2bbcef4116a3e43fb127caa8f45cebca967 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:25:46 +0900 Subject: [PATCH 058/120] =?UTF-8?q?fix:=20gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c847a6d..044c148 100644 --- a/.gitignore +++ b/.gitignore @@ -318,7 +318,9 @@ gradle-app.setting # yml *.yml -# CI/CD 워크플로우 파일은 추적 +# .github 디렉터리와 워크플로우 폴더는 무시하지 않기 +!.github/ +!.github/workflows/ !.github/workflows/ci-cd.yml # End of https://www.toptal.com/developers/gitignore/api/java,intellij,macos,gradle,git,maven,visualstudiocode,windows From bc45c9df209321a43cd32dbe1ae63081d76669df Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:29:02 +0900 Subject: [PATCH 059/120] =?UTF-8?q?fix:=20gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 044c148..c6d4325 100644 --- a/.gitignore +++ b/.gitignore @@ -315,12 +315,12 @@ gradle-app.setting # Java heap dump *.hprof -# yml +# 모든 yml 파일 무시 *.yml -# .github 디렉터리와 워크플로우 폴더는 무시하지 않기 -!.github/ -!.github/workflows/ -!.github/workflows/ci-cd.yml +# → 하지만 GitHub Actions 워크플로우는 예외 처리 +!/.github/ +!/.github/workflows/ +!/.github/workflows/ci-cd.yml # End of https://www.toptal.com/developers/gitignore/api/java,intellij,macos,gradle,git,maven,visualstudiocode,windows From 63008d2d1e49dd1f119e8bf488fec71770a19985 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:30:31 +0900 Subject: [PATCH 060/120] =?UTF-8?q?chore:=20ci/cd=20configuration=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..351633d --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,74 @@ +name: Build & Deploy to EC2 + +on: + push: + branches: [ dev ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Java 21 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: ${{ runner.os }}-gradle- + + - name: Build JAR with dev profile + run: ./gradlew bootJar --no-daemon -Dspring.profiles.active=dev + + - name: Docker Login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push Docker Image + uses: docker/build-push-action@v4 + with: + context: . + file: Dockerfile + push: true + tags: docker.io/${{ secrets.DOCKERHUB_USER }}/smart-meal-table:${{ github.sha }} + + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - name: Install SSH + run: sudo apt-get update && sudo apt-get install -y openssh-client + + - name: Setup SSH Key + env: + SSH_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + echo "$SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + echo -e "Host *\n\tStrictHostKeyChecking no\n" > ~/.ssh/config + + - name: Deploy to EC2 + run: | + ssh ubuntu@${{ secrets.EC2_HOST }} << 'EOF' + # 1) 기존 컨테이너 제거 + docker rm -f smart-meal-table || true + # 2) 최신 이미지 pull + docker pull docker.io/${{ secrets.DOCKERHUB_USER }}/smart-meal-table:${{ github.sha }} + # 3) 컨테이너 실행 (prod 프로파일) + docker run -d \ + --name smart-meal-table \ + -v /home/ubuntu/app:/config \ + -e SPRING_PROFILES_ACTIVE=prod \ + -p 8080:8080 \ + docker.io/${{ secrets.DOCKERHUB_USER }}/smart-meal-table:${{ github.sha }} + EOF From 83e746ca72bd6930c109d629c3c5bf1df7378312 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:31:22 +0900 Subject: [PATCH 061/120] =?UTF-8?q?fix:=20ci/cd=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 351633d..0dfab1f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: Build & Deploy to EC2 on: push: - branches: [ dev ] + branches: [ development ] jobs: build: From 9a929d13c629a4598a2892597ad6ae5e9da8505d Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 15 May 2025 23:49:05 +0900 Subject: [PATCH 062/120] =?UTF-8?q?fix:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0dfab1f..7b676e4 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -64,11 +64,11 @@ jobs: docker rm -f smart-meal-table || true # 2) 최신 이미지 pull docker pull docker.io/${{ secrets.DOCKERHUB_USER }}/smart-meal-table:${{ github.sha }} - # 3) 컨테이너 실행 (prod 프로파일) + # 3) 컨테이너 실행 (dev 프로파일) docker run -d \ --name smart-meal-table \ -v /home/ubuntu/app:/config \ - -e SPRING_PROFILES_ACTIVE=prod \ + -e SPRING_PROFILES_ACTIVE=dev \ -p 8080:8080 \ docker.io/${{ secrets.DOCKERHUB_USER }}/smart-meal-table:${{ github.sha }} EOF From 0f00b6885be638d6f9c0452b61f4f1274d6fc6ff Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Sun, 18 May 2025 23:59:54 +0900 Subject: [PATCH 063/120] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member => exception}/PasswordFailedExceededException.java | 2 +- .../{domain/member => exception}/PasswordPolicyException.java | 2 +- .../stcom/smartmealtable/domain/member/MemberPasswordTest.java | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) rename src/main/java/com/stcom/smartmealtable/{domain/member => exception}/PasswordFailedExceededException.java (94%) rename src/main/java/com/stcom/smartmealtable/{domain/member => exception}/PasswordPolicyException.java (93%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java b/src/main/java/com/stcom/smartmealtable/exception/PasswordFailedExceededException.java similarity index 94% rename from src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java rename to src/main/java/com/stcom/smartmealtable/exception/PasswordFailedExceededException.java index ddea7ef..cb2397b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordFailedExceededException.java +++ b/src/main/java/com/stcom/smartmealtable/exception/PasswordFailedExceededException.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.domain.member; +package com.stcom.smartmealtable.exception; public class PasswordFailedExceededException extends Exception { public PasswordFailedExceededException() { diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java b/src/main/java/com/stcom/smartmealtable/exception/PasswordPolicyException.java similarity index 93% rename from src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java rename to src/main/java/com/stcom/smartmealtable/exception/PasswordPolicyException.java index 94b453d..7f981c3 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/PasswordPolicyException.java +++ b/src/main/java/com/stcom/smartmealtable/exception/PasswordPolicyException.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.domain.member; +package com.stcom.smartmealtable.exception; public class PasswordPolicyException extends Exception { diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java index d8e0df2..fef9d7a 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; import org.junit.jupiter.api.Test; class MemberPasswordTest { From 6ca4df7ebfdeccde35e9bc6181992954f95e1272 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 15:10:06 +0900 Subject: [PATCH 064/120] =?UTF-8?q?refactor:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/{common => Address}/Address.java | 25 ++++++++++++++---- .../{common => Address}/AddressType.java | 2 +- .../smartmealtable/domain/Budget/Budget.java | 2 +- .../{ => domain}/common/BaseEntity.java | 2 +- .../{ => domain}/common/BaseTimeEntity.java | 2 +- .../domain/food/FoodPreference.java | 2 +- .../domain/member/MemberPassword.java | 2 ++ .../domain/member/MemberService.java | 24 ----------------- .../domain/social/SocialAccount.java | 2 +- .../social/GoogleHttpMessage.java | 6 ++--- .../social/KakaoHttpMessage.java | 6 ++--- .../social/SocialAuthService.java} | 26 +++++++++---------- .../social/SocialConst.java | 2 +- .../social/SocialHttpMessage.java | 4 +-- .../MemberRepository.java | 3 ++- .../SocialAccountRepository.java | 3 ++- .../security/JwtTokenService.java | 4 +-- .../dto/token/JwtTokenResponseDto.java | 2 +- .../{web => service}/dto/token/TokenDto.java | 2 +- 19 files changed, 58 insertions(+), 63 deletions(-) rename src/main/java/com/stcom/smartmealtable/domain/{common => Address}/Address.java (59%) rename src/main/java/com/stcom/smartmealtable/domain/{common => Address}/AddressType.java (52%) rename src/main/java/com/stcom/smartmealtable/{ => domain}/common/BaseEntity.java (91%) rename src/main/java/com/stcom/smartmealtable/{ => domain}/common/BaseTimeEntity.java (92%) delete mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java rename src/main/java/com/stcom/smartmealtable/{web/auth => infrastructure}/social/GoogleHttpMessage.java (95%) rename src/main/java/com/stcom/smartmealtable/{web/auth => infrastructure}/social/KakaoHttpMessage.java (95%) rename src/main/java/com/stcom/smartmealtable/{web/auth/social/SocialHttpMessageManager.java => infrastructure/social/SocialAuthService.java} (50%) rename src/main/java/com/stcom/smartmealtable/{web/auth => infrastructure}/social/SocialConst.java (71%) rename src/main/java/com/stcom/smartmealtable/{web/auth => infrastructure}/social/SocialHttpMessage.java (77%) rename src/main/java/com/stcom/smartmealtable/{domain/member => repository}/MemberRepository.java (72%) rename src/main/java/com/stcom/smartmealtable/{domain/social => repository}/SocialAccountRepository.java (86%) rename src/main/java/com/stcom/smartmealtable/{web => service}/dto/token/JwtTokenResponseDto.java (90%) rename src/main/java/com/stcom/smartmealtable/{web => service}/dto/token/TokenDto.java (93%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java similarity index 59% rename from src/main/java/com/stcom/smartmealtable/domain/common/Address.java rename to src/main/java/com/stcom/smartmealtable/domain/Address/Address.java index 81db5ff..a58fd7b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/common/Address.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.domain.common; +package com.stcom.smartmealtable.domain.Address; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -7,11 +7,13 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import java.math.BigDecimal; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@NoArgsConstructor public class Address { @Id @@ -27,15 +29,28 @@ public class Address { private String alias; - private BigDecimal latitude; + private Double latitude; - private BigDecimal longitude; + private Double longitude; @Enumerated(EnumType.STRING) private AddressType type; + @Builder + public Address(String lotNumberAddress, String roadAddress, String detailAddress, String alias, Double latitude, + Double longitude, AddressType type) { + this.lotNumberAddress = lotNumberAddress; + this.roadAddress = roadAddress; + this.detailAddress = detailAddress; + this.alias = alias; + this.latitude = latitude; + this.longitude = longitude; + this.type = type; + } + + public void updateAddress(String lotNumberAddress, String roadAddress, String detailAddress, - BigDecimal latitude, BigDecimal longitude) { + Double latitude, Double longitude) { this.lotNumberAddress = lotNumberAddress; this.roadAddress = roadAddress; this.detailAddress = detailAddress; diff --git a/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java similarity index 52% rename from src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java rename to src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java index a83f75f..6784561 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/common/AddressType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.domain.common; +package com.stcom.smartmealtable.domain.Address; public enum AddressType { HOME, SCHOOL, OFFICE diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index d194ead..c0976d1 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.Budget; -import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java b/src/main/java/com/stcom/smartmealtable/domain/common/BaseEntity.java similarity index 91% rename from src/main/java/com/stcom/smartmealtable/common/BaseEntity.java rename to src/main/java/com/stcom/smartmealtable/domain/common/BaseEntity.java index ec4116e..9e51b38 100644 --- a/src/main/java/com/stcom/smartmealtable/common/BaseEntity.java +++ b/src/main/java/com/stcom/smartmealtable/domain/common/BaseEntity.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.common; +package com.stcom.smartmealtable.domain.common; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; diff --git a/src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java b/src/main/java/com/stcom/smartmealtable/domain/common/BaseTimeEntity.java similarity index 92% rename from src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java rename to src/main/java/com/stcom/smartmealtable/domain/common/BaseTimeEntity.java index 04b022c..fc35204 100644 --- a/src/main/java/com/stcom/smartmealtable/common/BaseTimeEntity.java +++ b/src/main/java/com/stcom/smartmealtable/domain/common/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.common; +package com.stcom.smartmealtable.domain.common; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java index d7074ae..a6657e1 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.food; -import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java index 1fbfc74..c60f9a3 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -1,6 +1,8 @@ package com.stcom.smartmealtable.domain.member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; import jakarta.persistence.Embeddable; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java deleted file mode 100644 index 6a838f4..0000000 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.stcom.smartmealtable.domain.member; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class MemberService { - - private final MemberRepository memberRepository; - - public boolean isEmailExists(String email) { - List findMembers = memberRepository.findMemberByEmail(email); - if (findMembers.isEmpty()) { - return false; - } - return true; - } - - public void saveMember(Member member) { - memberRepository.save(member); - } -} diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index ed9e87e..12d1d78 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.social; -import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java similarity index 95% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java index ca351cb..d6353e2 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/GoogleHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java @@ -1,12 +1,12 @@ -package com.stcom.smartmealtable.web.auth.social; +package com.stcom.smartmealtable.infrastructure.social; -import static com.stcom.smartmealtable.web.auth.social.SocialConst.GOOGLE; +import static com.stcom.smartmealtable.infrastructure.social.SocialConst.GOOGLE; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.stcom.smartmealtable.web.dto.token.TokenDto; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import java.nio.charset.StandardCharsets; import java.util.Base64; import lombok.Data; diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java similarity index 95% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java index c5c18ec..ab5c7c9 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/KakaoHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java @@ -1,12 +1,12 @@ -package com.stcom.smartmealtable.web.auth.social; +package com.stcom.smartmealtable.infrastructure.social; -import static com.stcom.smartmealtable.web.auth.social.SocialConst.KAKAO; +import static com.stcom.smartmealtable.infrastructure.social.SocialConst.KAKAO; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.stcom.smartmealtable.web.dto.token.TokenDto; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java similarity index 50% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java index fa1a061..cb11aef 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessageManager.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java @@ -1,31 +1,29 @@ -package com.stcom.smartmealtable.web.auth.social; +package com.stcom.smartmealtable.infrastructure.social; -import static com.stcom.smartmealtable.web.auth.social.SocialConst.GOOGLE; -import static com.stcom.smartmealtable.web.auth.social.SocialConst.KAKAO; +import static com.stcom.smartmealtable.infrastructure.social.SocialConst.GOOGLE; +import static com.stcom.smartmealtable.infrastructure.social.SocialConst.KAKAO; -import com.stcom.smartmealtable.web.dto.token.TokenDto; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import jakarta.annotation.PostConstruct; +import jakarta.validation.constraints.NotEmpty; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; -@Component +@Service @RequiredArgsConstructor -public class SocialHttpMessageManager { +public class SocialAuthService { private final Map socialMap = new HashMap<>(); private final KakaoHttpMessage kakaoHttpMessage; private final GoogleHttpMessage googleHttpMessage; + private final RestClient client = RestClient.create(); - public RequestBodySpec getTokenRequestMessage(RestClient client, String provider, String code) { - return socialMap.get(provider).getRequestMessage(client, code); - } - - public TokenDto getTokenResponse(ResponseSpec responseSpec, String provider) { + public TokenDto getTokenResponse(@NotEmpty String provider, @NotEmpty String code) { + ResponseSpec responseSpec = socialMap.get(provider).getRequestMessage(client, code).retrieve(); return socialMap.get(provider).getTokenResponse(responseSpec); } @@ -34,4 +32,6 @@ public void init() { socialMap.put(KAKAO, kakaoHttpMessage); socialMap.put(GOOGLE, googleHttpMessage); } + + } diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialConst.java similarity index 71% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialConst.java index f8ad30d..41dd1db 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialConst.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialConst.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.web.auth.social; +package com.stcom.smartmealtable.infrastructure.social; public abstract class SocialConst { diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java similarity index 77% rename from src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java index 57fe22f..b20d7b2 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/social/SocialHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java @@ -1,6 +1,6 @@ -package com.stcom.smartmealtable.web.auth.social; +package com.stcom.smartmealtable.infrastructure.social; -import com.stcom.smartmealtable.web.dto.token.TokenDto; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java similarity index 72% rename from src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java rename to src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java index 5fca764..04674a3 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java @@ -1,5 +1,6 @@ -package com.stcom.smartmealtable.domain.member; +package com.stcom.smartmealtable.repository; +import com.stcom.smartmealtable.domain.member.Member; import jakarta.validation.constraints.Email; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java similarity index 86% rename from src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java rename to src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java index 4352cc0..23b119c 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java @@ -1,5 +1,6 @@ -package com.stcom.smartmealtable.domain.social; +package com.stcom.smartmealtable.repository; +import com.stcom.smartmealtable.domain.social.SocialAccount; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java index f1f841e..4127570 100644 --- a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java +++ b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java @@ -1,8 +1,8 @@ package com.stcom.smartmealtable.security; import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberRepository; -import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java similarity index 90% rename from src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java rename to src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java index 88ac8ed..8ab81a1 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/token/JwtTokenResponseDto.java +++ b/src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.web.dto.token; +package com.stcom.smartmealtable.service.dto.token; import lombok.Data; import lombok.ToString; diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java similarity index 93% rename from src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java rename to src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java index 2d6156a..19b7bed 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/token/TokenDto.java +++ b/src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.web.dto.token; +package com.stcom.smartmealtable.service.dto.token; import lombok.Builder; import lombok.Data; From 1c60575dd94e9f5dcce7c050e686cf500c1397bc Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 15:16:20 +0900 Subject: [PATCH 065/120] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A7=A4=ED=95=91=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/Member.java | 7 +++++-- .../domain/member/MemberProfile.java | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 93614d8..a456ddc 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -1,6 +1,9 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -36,7 +39,7 @@ public class Member extends BaseTimeEntity { // TODO: 이메일 인증 기능 구현해야함 private boolean isEmailVerified = true; - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL) @JoinColumn(name = "member_profile_id") private MemberProfile memberProfile; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index c60c4b6..217734d 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -1,7 +1,7 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.common.BaseTimeEntity; -import com.stcom.smartmealtable.domain.common.Address; +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -15,8 +15,11 @@ import jakarta.persistence.OneToOne; import java.util.ArrayList; import java.util.List; +import lombok.Builder; +import lombok.NoArgsConstructor; @Entity +@NoArgsConstructor public class MemberProfile extends BaseTimeEntity { @Id @@ -24,7 +27,7 @@ public class MemberProfile extends BaseTimeEntity { @Column(name = "member_profile_id") private Long id; - @OneToOne(mappedBy = "memberProfile") + @OneToOne(mappedBy = "memberProfile", orphanRemoval = true) private Member member; @Enumerated(EnumType.STRING) @@ -38,6 +41,16 @@ public class MemberProfile extends BaseTimeEntity { @JoinColumn(name = "member_id") private List
addressHistory = new ArrayList<>(); + @Builder + public MemberProfile(Member member, MemberGroup memberGroup, Long groupCode, String nickName, + List
addressHistory) { + this.member = member; + this.memberGroup = memberGroup; + this.groupCode = groupCode; + this.nickName = nickName; + this.addressHistory = addressHistory; + } + protected void linkMemberAuth(Member member) { this.member = member; } From 4517d22b90a4b7c5b703bb0ed66dd7aea2985058 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 15:17:07 +0900 Subject: [PATCH 066/120] =?UTF-8?q?refactor:=20infra=20=EA=B3=84=EC=B8=B5?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/service/MemberService.java | 40 +++++++ .../SocialAccountService.java | 13 ++- .../web/auth/OAuth2Controller.java | 18 ++- .../web/member/MemberController.java | 108 +++++++++++------- 4 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/service/MemberService.java rename src/main/java/com/stcom/smartmealtable/{domain/social => service}/SocialAccountService.java (78%) diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java new file mode 100644 index 0000000..ca5e793 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -0,0 +1,40 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + public void validateDuplicatedEmail(String email) { + List findMembers = memberRepository.findMemberByEmail(email); + if (!findMembers.isEmpty()) { + throw new IllegalArgumentException("이미 존재하는 이메일 입니다"); + } + } + + @Transactional + public void saveMember(Member member) { + memberRepository.save(member); + } + + public void checkPasswordDoubly(String password, String confirmPassword) { + if (!password.equals(confirmPassword)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다"); + } + } + + @Transactional + public void linkMember(Long id, MemberProfile profile) { + Member member = memberRepository.findById(id).orElseThrow(() -> new IllegalStateException("존재하지 않는 회원입니다")); + member.registerMemberProfile(profile); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java similarity index 78% rename from src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java rename to src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java index 54392de..60d6805 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccountService.java +++ b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java @@ -1,8 +1,10 @@ -package com.stcom.smartmealtable.domain.social; +package com.stcom.smartmealtable.service; import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberRepository; -import com.stcom.smartmealtable.web.dto.token.TokenDto; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -42,10 +44,11 @@ public boolean isNewUser(String provider, String providerUserId) { } @Transactional - public void updateToken(SocialAccount socialAccount, String accessToken, String refreshToken, + public void updateToken(Long socialAccountId, String accessToken, String refreshToken, LocalDateTime tokenExpiresAt) { + SocialAccount socialAccount = socialAccountRepository.findById(socialAccountId) + .orElseThrow(() -> new IllegalStateException("확인되지 않은 계정입니다")); socialAccount.updateToken(accessToken, refreshToken, tokenExpiresAt); - socialAccountRepository.save(socialAccount); } public Long findMemberId(String provider, String providerUserId) { diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java index e9adbed..5d4ad92 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java @@ -1,12 +1,12 @@ package com.stcom.smartmealtable.web.auth; -import com.stcom.smartmealtable.domain.social.SocialAccountService; +import com.stcom.smartmealtable.infrastructure.social.SocialAuthService; import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.web.auth.social.SocialHttpMessageManager; +import com.stcom.smartmealtable.service.SocialAccountService; +import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; +import com.stcom.smartmealtable.service.dto.token.TokenDto; import com.stcom.smartmealtable.web.dto.ApiResponse; -import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; -import com.stcom.smartmealtable.web.dto.token.TokenDto; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; @@ -16,8 +16,6 @@ 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.client.RestClient; -import org.springframework.web.client.RestClient.RequestBodySpec; @RestController @Slf4j @@ -25,17 +23,15 @@ @RequiredArgsConstructor public class OAuth2Controller { - private final SocialHttpMessageManager socialManager; - private final RestClient client = RestClient.create(); + private final SocialAuthService socialManager; private final SocialAccountService socialAccountService; private final JwtTokenService jwtTokenService; + private final SocialAuthService socialAuthService; @PostMapping("/oauth2/code") public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { log.info("request = {}", request); - RequestBodySpec tokenRequestMessage = socialManager.getTokenRequestMessage(client, request.getProvider(), - request.getAuthorizationCode()); - TokenDto token = socialManager.getTokenResponse(tokenRequestMessage.retrieve(), request.getProvider()); + TokenDto token = socialAuthService.getTokenResponse(request.getProvider(), request.getAuthorizationCode()); log.info("response 성공 = {}", token); boolean isNewUser = socialAccountService.isNewUser(token.getProvider(), token.getProviderUserId()); diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java index 90f5e74..92f42bc 100644 --- a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java @@ -1,19 +1,26 @@ package com.stcom.smartmealtable.web.member; -import com.stcom.smartmealtable.domain.common.Address; +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.food.FoodCategory; import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberGroup; -import com.stcom.smartmealtable.domain.member.MemberService; -import com.stcom.smartmealtable.domain.member.PasswordPolicyException; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.infrastructure.AddressApiService; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequestDto; +import com.stcom.smartmealtable.security.JwtAuthorization; import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.MemberService; +import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; import com.stcom.smartmealtable.web.dto.ApiResponse; -import com.stcom.smartmealtable.web.dto.token.JwtTokenResponseDto; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; +import java.util.ArrayList; import java.util.List; -import java.util.Map; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -24,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @@ -34,53 +42,51 @@ public class MemberController { private final MemberService memberService; private final JwtTokenService jwtTokenService; + private final AddressApiService addressApiService; @GetMapping("/email/check") public ResponseEntity> checkEmail(@Email @RequestParam String email) { - if (memberService.isEmailExists(email)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("이미 존재하는 이메일입니다.")); - } + memberService.validateDuplicatedEmail(email); return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); } + @ResponseStatus(HttpStatus.CREATED) @PostMapping() - public ResponseEntity> createMember(@Valid @RequestBody CreateMemberRequest request, - BindingResult bindingResult) { - if (bindingResult.hasErrors()) { - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) - .body(ApiResponse.createFail(bindingResult)); - } + public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, + BindingResult bindingResult) throws PasswordPolicyException { + memberService.validateDuplicatedEmail(request.getEmail()); + memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); - if (memberService.isEmailExists(request.getEmail())) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("이미 존재하는 이메일입니다.")); - } - - if (!request.getPassword().equals(request.getConfirmPassword())) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError("비밀번호가 일치하지 않습니다.")); - } + Member member = Member.builder() + .fullName(request.getFullName()) + .email(request.getEmail()) + .rawPassword(request.getPassword()) + .build(); - Member member; - try { - member = Member.builder() - .fullName(request.getFullName()) - .email(request.getEmail()) - .rawPassword(request.getPassword()) - .build(); - } catch (PasswordPolicyException e) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponse.createError(e.getMessage())); - } memberService.saveMember(member); JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId()); tokenDto.setNewUser(true); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.createSuccess(tokenDto)); + return ApiResponse.createSuccess(tokenDto); } -// @PostMapping("/profile") -// public ApiResponse createMemberProfile(@JwtAuthorization Member member, -// @RequestBody CreateMemberProfileRequest request) { -// -// } + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/profile") + public ApiResponse createMemberProfile(@JwtAuthorization Member member, + @RequestBody CreateMemberProfileRequest request) { + // request 주소로 부터 위도, 경도 얻기(카카오 local api) + Address address = getAddressFromApiService(request.getHomeAddress()); + MemberProfile memberProfile = request.toEntity(); + memberProfile.addAddress(address); + memberService.linkMember(member.getId(), memberProfile); + + return ApiResponse.createSuccessWithNoContent(); + } + + private Address getAddressFromApiService(AddressRequest request) { + AddressRequestDto dto = new AddressRequestDto(request.getRoadAddress(), AddressType.HOME, "집", + request.getDetailAddress()); + return addressApiService.createAddressFromRequest(dto); + } @Data @AllArgsConstructor @@ -94,15 +100,33 @@ static class CreateMemberRequest { } @Data - @AllArgsConstructor + @NoArgsConstructor static class CreateMemberProfileRequest { private MemberGroup groupType; private Long groupCode; - private Address homeAddress; - private Map foodPreference; - private List hateFoods; + private AddressRequest homeAddress; + private List foodPreference; + private List hateFoods; private Long dailyLimitAmount; private Long monthlyLimitAmount; + + public MemberProfile toEntity() { + return MemberProfile.builder() + .memberGroup(groupType) + .groupCode(groupCode) + .nickName("test") + .addressHistory(new ArrayList<>()) + .build(); + } + } + + @Data + @AllArgsConstructor + static class AddressRequest { + private int zonecode; + private String lotNumberAddress; + private String roadAddress; + private String detailAddress; } From b2f33d198710c3605196764fab92e3813a8d29ab Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 15:18:05 +0900 Subject: [PATCH 067/120] =?UTF-8?q?feat:=20=EC=A3=BC=EC=86=8C=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=20=EB=B3=80=ED=99=98=20client=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/ExternApiStatusError.java | 7 + .../infrastructure/AddressApiService.java | 153 ++++++++++++++++++ .../infrastructure/dto/AddressRequestDto.java | 18 +++ .../repository/AddressRepository.java | 7 + .../service/AddressService.java | 12 ++ 5 files changed, 197 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/exception/ExternApiStatusError.java create mode 100644 src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java create mode 100644 src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/AddressService.java diff --git a/src/main/java/com/stcom/smartmealtable/exception/ExternApiStatusError.java b/src/main/java/com/stcom/smartmealtable/exception/ExternApiStatusError.java new file mode 100644 index 0000000..a3b9b78 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/exception/ExternApiStatusError.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.exception; + +public class ExternApiStatusError extends RuntimeException { + public ExternApiStatusError(String message) { + super(message); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java new file mode 100644 index 0000000..85a7478 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java @@ -0,0 +1,153 @@ +package com.stcom.smartmealtable.infrastructure; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequestDto; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +public class AddressApiService { + + @Value("${kakao.oauth.client-id}") + private String clientId; + + private final RestClient client = RestClient.create(); + + public Address createAddressFromRequest(AddressRequestDto requestDto) { + AddressSearchResponse addressSearchResponse = client.get() + .uri(uriBuilder -> uriBuilder + .scheme("https") + .host("dapi.kakao.com") + .path("/v2/local/search/address") + .queryParam("query", requestDto.getRoadAddress()) + .build()) + .header("Authorization", "KakaoAK " + clientId) + .retrieve() + .body(AddressSearchResponse.class); + + if (addressSearchResponse.getMeta().getTotalCount() >= 2) { + throw new IllegalArgumentException("주소가 모호합니다. 정확한 주소를 입력하세요"); + } + + return Address.builder() + .type(requestDto.getAddressType()) + .alias(requestDto.getAlias()) + .longitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLongitude())) + .latitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLatitude())) + .lotNumberAddress(addressSearchResponse.getDocuments().getFirst().getAddress().getAddressName()) + .roadAddress(addressSearchResponse.getDocuments().getFirst().getRoadAddress().getAddressName()) + .detailAddress(requestDto.getDetailAddress()) + .build(); + } + + @Data + @AllArgsConstructor + static class AddressSearchResponse { + private Meta meta; + private List documents; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Meta { + private Integer totalCount; + private Integer pageableCount; + private Boolean isEnd; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Document { + private String addressName; + + private String addressType; + + @JsonProperty("x") + private String longitude; + + @JsonProperty("y") + private String latitude; + + private LotAddress address; + + private RoadAddress roadAddress; + + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class LotAddress { + private String addressName; + + @JsonProperty("region_1depth_name") + private String region1depthName; + + @JsonProperty("region_2depth_name") + private String region2depthName; + + @JsonProperty("region_3depth_name") + private String region3depthName; + + @JsonProperty("region_3depth_h_name") + private String region3depthHName; + + private String hCode; + + private String bCode; + + private String mountainYn; + + private String mainAddressNo; + + private String subAddressNo; + + private String x; + private String y; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class RoadAddress { + private String addressName; + + @JsonProperty("region_1depth_name") + private String region1depthName; + + @JsonProperty("region_2depth_name") + private String region2depthName; + + @JsonProperty("region_3depth_name") + private String region3depthName; + + private String roadName; + + @JsonProperty("underground_yn") + private String undergroundYn; + + @JsonProperty("main_building_no") + private String mainBuildingNo; + + @JsonProperty("sub_building_no") + private String subBuildingNo; + + @JsonProperty("building_name") + private String buildingName; + + @JsonProperty("zone_no") + private String zoneNo; + + private String x; + private String y; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java new file mode 100644 index 0000000..b6bd9af --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java @@ -0,0 +1,18 @@ +package com.stcom.smartmealtable.infrastructure.dto; + +import com.stcom.smartmealtable.domain.Address.AddressType; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AddressRequestDto { + + private String roadAddress; + + private AddressType addressType; + + private String alias; + + private String detailAddress; +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java b/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java new file mode 100644 index 0000000..8daa45a --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.Address.Address; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AddressRepository extends JpaRepository { +} diff --git a/src/main/java/com/stcom/smartmealtable/service/AddressService.java b/src/main/java/com/stcom/smartmealtable/service/AddressService.java new file mode 100644 index 0000000..87ae77a --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/AddressService.java @@ -0,0 +1,12 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.repository.AddressRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AddressService { + + private final AddressRepository addressRepository; +} From 5293e575744fac8a6142d3e791a98a980aee201c Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 15:59:18 +0900 Subject: [PATCH 068/120] =?UTF-8?q?fix:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=B9=BC=EB=9F=BC=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 예약어 충돌 수정 --- .../smartmealtable/domain/Budget/Budget.java | 4 +++- .../domain/Budget/DailyBudget.java | 5 ++++- .../domain/Budget/MonthlyBudget.java | 8 +++++++- .../persistence/YearMonthConverter.java | 20 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/infrastructure/persistence/YearMonthConverter.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index c0976d1..a3f0863 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -4,6 +4,7 @@ import com.stcom.smartmealtable.domain.member.Member; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -16,7 +17,7 @@ import lombok.NoArgsConstructor; @Getter -//@Entity +@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn @NoArgsConstructor @@ -33,6 +34,7 @@ public abstract class Budget extends BaseTimeEntity { private BigDecimal spendAmount = BigDecimal.ZERO; + @Column(name = "budget_limit") private BigDecimal limit; protected Budget(Member member, BigDecimal limit) { diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java index 6ca90c4..4651133 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -1,12 +1,14 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.domain.member.Member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.LocalDate; import lombok.Getter; import lombok.NoArgsConstructor; -//@Entity +@Entity @Getter @NoArgsConstructor public class DailyBudget extends Budget { @@ -17,5 +19,6 @@ public DailyBudget(Member member, BigDecimal limit, this.date = date; } + @Column(name = "daily_budget_date") private LocalDate date; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java index 6fc98e8..f64924b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -1,12 +1,16 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.infrastructure.persistence.YearMonthConverter; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; import java.math.BigDecimal; import java.time.YearMonth; import lombok.Getter; import lombok.NoArgsConstructor; -//@Entity +@Entity @Getter @NoArgsConstructor public class MonthlyBudget extends Budget { @@ -17,5 +21,7 @@ public MonthlyBudget(Member member, BigDecimal limit, this.yearMonth = yearMonth; } + @Convert(converter = YearMonthConverter.class) + @Column(name = "budget_year_month") private YearMonth yearMonth; } diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/persistence/YearMonthConverter.java b/src/main/java/com/stcom/smartmealtable/infrastructure/persistence/YearMonthConverter.java new file mode 100644 index 0000000..077d9dd --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/persistence/YearMonthConverter.java @@ -0,0 +1,20 @@ +package com.stcom.smartmealtable.infrastructure.persistence; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.sql.Date; +import java.time.YearMonth; + +@Converter(autoApply = true) +public class YearMonthConverter implements AttributeConverter { + + @Override + public Date convertToDatabaseColumn(YearMonth attribute) { + return (attribute == null ? null : Date.valueOf(attribute.atDay(1))); + } + + @Override + public YearMonth convertToEntityAttribute(Date dbData) { + return (dbData == null ? null : YearMonth.from(dbData.toLocalDate())); + } +} From 485596581468417f3a0339e58287f40e95cab014 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 23:32:58 +0900 Subject: [PATCH 069/120] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20-=20?= =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B3=84=EC=A0=95=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/Member.java | 4 +++ .../dto}/TokenDto.java | 6 ++-- .../social/GoogleHttpMessage.java | 22 +++++++++++-- .../social/KakaoHttpMessage.java | 25 +++++++++++++- .../repository/MemberRepository.java | 4 +-- .../repository/SocialAccountRepository.java | 3 ++ .../service/SocialAccountService.java | 33 +++++++++++++++++-- .../web/auth/OAuth2Controller.java | 23 ++++++++----- 8 files changed, 101 insertions(+), 19 deletions(-) rename src/main/java/com/stcom/smartmealtable/{service/dto/token => infrastructure/dto}/TokenDto.java (80%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index a456ddc..ba6d121 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -43,6 +43,10 @@ public class Member extends BaseTimeEntity { @JoinColumn(name = "member_profile_id") private MemberProfile memberProfile; + public Member(String email) { + this.email = email; + } + @Builder public Member(String fullName, String email, String rawPassword) throws PasswordPolicyException { this.fullName = fullName; diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/TokenDto.java similarity index 80% rename from src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/dto/TokenDto.java index 19b7bed..7793edf 100644 --- a/src/main/java/com/stcom/smartmealtable/service/dto/token/TokenDto.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/TokenDto.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.service.dto.token; +package com.stcom.smartmealtable.infrastructure.dto; import lombok.Builder; import lombok.Data; @@ -14,15 +14,17 @@ public class TokenDto { private String tokenType; private String provider; private String providerUserId; + private String email; @Builder public TokenDto(String accessToken, String refreshToken, Integer expiresIn, String tokenType, String provider, - String providerUserId) { + String providerUserId, String email) { this.accessToken = accessToken; this.refreshToken = refreshToken; this.expiresIn = expiresIn; this.tokenType = tokenType; this.provider = provider; this.providerUserId = providerUserId; + this.email = email; } } diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java index d6353e2..a683a4d 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/GoogleHttpMessage.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.stcom.smartmealtable.service.dto.token.TokenDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import java.nio.charset.StandardCharsets; import java.util.Base64; import lombok.Data; @@ -68,10 +68,10 @@ public TokenDto getTokenResponse(ResponseSpec responseSpec) { .tokenType(tokenResponse.getTokenType()) .provider(GOOGLE) .providerUserId(extractProviderUserId(tokenResponse.getIdToken())) + .email(extractEmail(tokenResponse.getIdToken())) .build(); } - @Override public String extractProviderUserId(String idToken) { if (idToken == null || idToken.isBlank()) { @@ -94,6 +94,24 @@ public String extractProviderUserId(String idToken) { } } + public String extractEmail(String idToken) { + if (idToken == null || idToken.isBlank()) { + return null; + } + try { + String[] parts = idToken.split("\\."); + if (parts.length != 3) { + return null; + } + String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + JsonNode node = new ObjectMapper().readTree(payload); + return node.has("email") ? node.get("email").asText() : null; + } catch (Exception e) { + log.error("Google ID 토큰에서 email 파싱 실패: {}", e.getMessage()); + return null; + } + } + @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) static class GoogleTokenResponse { diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java index ab5c7c9..26135af 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/KakaoHttpMessage.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.stcom.smartmealtable.service.dto.token.TokenDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -62,6 +62,7 @@ public TokenDto getTokenResponse(ResponseSpec responseSpec) { .tokenType(tokenResponse.getTokenType()) .provider(KAKAO) .providerUserId(extractProviderUserId(tokenResponse.getIdToken())) + .email(extractEmail(tokenResponse.getIdToken())) .build(); } @@ -91,6 +92,28 @@ public String extractProviderUserId(String idToken) { } } + public String extractEmail(String idToken) { + if (idToken == null || idToken.isEmpty()) { + return null; + } + try { + String[] parts = idToken.split("\\."); + if (parts.length != 3) { + return null; + } + String payloadJson = new String( + java.util.Base64.getUrlDecoder().decode(parts[1]), + java.nio.charset.StandardCharsets.UTF_8 + ); + + JsonNode node = new ObjectMapper().readTree(payloadJson); + return node.has("email") ? node.get("email").asText() : null; + } catch (Exception e) { + log.error("카카오 ID 토큰에서 email 파싱 실패", e); + return null; + } + } + @Data @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) static class KakaoTokenResponse { diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java index 04674a3..ff8795e 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java @@ -2,10 +2,10 @@ import com.stcom.smartmealtable.domain.member.Member; import jakarta.validation.constraints.Email; -import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { - List findMemberByEmail(@Email String email); + Optional findMemberByEmail(@Email String email); } diff --git a/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java index 23b119c..ebb10cc 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java @@ -1,6 +1,7 @@ package com.stcom.smartmealtable.repository; import com.stcom.smartmealtable.domain.social.SocialAccount; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -15,4 +16,6 @@ Optional findMemberIdByProviderAndProviderUserId( @Param("provider") String provider, @Param("providerUserId") String providerUserId ); + + List findAllByMemberId(Long memberId); } diff --git a/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java index 60d6805..552037a 100644 --- a/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java +++ b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java @@ -2,10 +2,11 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import com.stcom.smartmealtable.repository.MemberRepository; import com.stcom.smartmealtable.repository.SocialAccountRepository; -import com.stcom.smartmealtable.service.dto.token.TokenDto; import java.time.LocalDateTime; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,8 +20,8 @@ public class SocialAccountService { private final MemberRepository memberRepository; @Transactional - public void createNewAccount(TokenDto tokenDto) { - Member member = new Member(); + public void createNewMemberAndLinkSocialAccount(TokenDto tokenDto) { + Member member = new Member(tokenDto.getEmail()); memberRepository.save(member); SocialAccount socialAccount = SocialAccount.builder() @@ -35,6 +36,23 @@ public void createNewAccount(TokenDto tokenDto) { socialAccountRepository.save(socialAccount); } + @Transactional + public void linkSocialAccount(TokenDto tokenDto) { + Member member = memberRepository.findMemberByEmail(tokenDto.getEmail()) + .orElseThrow(() -> new IllegalStateException("회원이 null일 수는 없습니다")); + + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider(tokenDto.getProvider()) + .providerUserId(tokenDto.getProviderUserId()) + .tokenType(tokenDto.getTokenType()) + .accessToken(tokenDto.getAccessToken()) + .refreshToken(tokenDto.getRefreshToken()) + .tokenExpiresAt(LocalDateTime.now().plusSeconds(tokenDto.getExpiresIn())) + .build(); + socialAccountRepository.save(socialAccount); + } + public SocialAccount findSocialAccount(String provider, String providerUserId) { return socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId).orElse(null); } @@ -55,4 +73,13 @@ public Long findMemberId(String provider, String providerUserId) { return socialAccountRepository.findMemberIdByProviderAndProviderUserId(provider, providerUserId) .orElseThrow(() -> new IllegalStateException("회원 정보가 없습니다. 먼저 회원 정보를 생성해주세요")); } + + public List findAllProviders(Long memberId) { + List accounts = socialAccountRepository.findAllByMemberId(memberId); + return accounts.stream() + .map(SocialAccount::getProvider) + .toList(); + } + + } diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java index 5d4ad92..4d7fa3e 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java @@ -1,11 +1,12 @@ package com.stcom.smartmealtable.web.auth; -import com.stcom.smartmealtable.infrastructure.social.SocialAuthService; +import com.stcom.smartmealtable.infrastructure.SocialAuthService; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.MemberService; import com.stcom.smartmealtable.service.SocialAccountService; -import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; -import com.stcom.smartmealtable.service.dto.token.TokenDto; import com.stcom.smartmealtable.web.dto.ApiResponse; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -27,22 +28,26 @@ public class OAuth2Controller { private final SocialAccountService socialAccountService; private final JwtTokenService jwtTokenService; private final SocialAuthService socialAuthService; + private final MemberService memberService; @PostMapping("/oauth2/code") public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { log.info("request = {}", request); - TokenDto token = socialAuthService.getTokenResponse(request.getProvider(), request.getAuthorizationCode()); + TokenDto token = socialAuthService.getTokenResponse(request.getProvider().toLowerCase(), + request.getAuthorizationCode()); log.info("response 성공 = {}", token); - boolean isNewUser = socialAccountService.isNewUser(token.getProvider(), + boolean isNewMember = memberService.isNewMember(token.getEmail()); + boolean isSocialNewUser = socialAccountService.isNewUser(token.getProvider(), token.getProviderUserId()); - log.info("새로운 멤버인지 확인 = {}", isNewUser); - if (isNewUser) { - socialAccountService.createNewAccount(token); + if (isNewMember && isSocialNewUser) { // 소셜, 회원 모두 신규 + socialAccountService.createNewMemberAndLinkSocialAccount(token); + } else if (isSocialNewUser) { // 소셜만 신규, 회원은 존재 + socialAccountService.linkSocialAccount(token); } JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto( socialAccountService.findMemberId(token.getProvider(), token.getProviderUserId())); - tokenDto.setNewUser(isNewUser); + tokenDto.setNewUser(isSocialNewUser); log.info("response = {}", tokenDto); return ApiResponse.createSuccess(tokenDto); } From 185eb19474860b0a567c08e7eb18d3ff4892a457 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 23:33:54 +0900 Subject: [PATCH 070/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/BudgetRepository.java | 25 ++++++ .../repository/FoodPreferenceRepository.java | 11 +++ .../repository/MemberProfileRepository.java | 13 +++ .../smartmealtable/service/BudgetService.java | 23 ++++++ .../service/FoodPreferenceService.java | 23 ++++++ .../smartmealtable/service/MemberService.java | 18 ++-- .../web/member/MemberController.java | 82 +++++++++++++++++-- 7 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/BudgetService.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java diff --git a/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java b/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java new file mode 100644 index 0000000..4c7f86b --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java @@ -0,0 +1,25 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.Budget.Budget; +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface BudgetRepository extends JpaRepository { + + @Query("select b from Budget b where type(b) = DailyBudget and b.member.id = :memberId") + List findDailyBudgetsViaType(@Param("memberId") Long memberId); + + @Query("select b from Budget b where type(b) = DailyBudget and b.member.id = :memberId order by treat (b as DailyBudget).date desc") + Optional findFirstDailyBudgetByMemberId(@Param("memberId") Long memberId); + + @Query("select b from Budget b where type(b) = MonthlyBudget and b.member.id = :memberId") + List findMonthlyBudgetsViaType(@Param("memberId") Long memberId); + + @Query("select b from Budget b where type(b) = MonthlyBudget and b.member.id = :memberId order by treat (b as MonthlyBudget ).yearMonth desc") + Optional findFirstMonthlyBudgetByMemberId(@Param("memberId") Long memberId); +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java b/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java new file mode 100644 index 0000000..82263a8 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java @@ -0,0 +1,11 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.food.FoodPreference; +import com.stcom.smartmealtable.domain.member.Member; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FoodPreferenceRepository extends JpaRepository { + + List findFoodPreferencesByMember(Member member); +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java new file mode 100644 index 0000000..0058910 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java @@ -0,0 +1,13 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.member.MemberProfile; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MemberProfileRepository extends JpaRepository { + + @Query("select mp from MemberProfile mp where mp.member.id = :memberId") + Optional findMemberProfileByMemberId(@Param("memberId") Long memberId); +} diff --git a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java new file mode 100644 index 0000000..9ac53a4 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java @@ -0,0 +1,23 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.repository.BudgetRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BudgetService { + + private final BudgetRepository budgetRepository; + + public DailyBudget findRecentDailyBudgetByMemberId(Long memberId) { + return budgetRepository.findFirstDailyBudgetByMemberId(memberId).orElse(null); + } + + public MonthlyBudget findRecentMonthlyBudgetByMemberId(Long memberId) { + return budgetRepository.findFirstMonthlyBudgetByMemberId(memberId).orElse(null); + } + +} diff --git a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java new file mode 100644 index 0000000..9451cba --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java @@ -0,0 +1,23 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.FoodPreference; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.repository.FoodPreferenceRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FoodPreferenceService { + + private final FoodPreferenceRepository foodPreferenceRepository; + + public List findPreferredFoodCategories(Member member) { + return foodPreferenceRepository.findFoodPreferencesByMember(member) + .stream() + .map(FoodPreference::getCategory) + .toList(); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index ca5e793..258cf29 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -2,8 +2,8 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,12 +13,10 @@ public class MemberService { private final MemberRepository memberRepository; + private final MemberProfileRepository memberProfileRepository; public void validateDuplicatedEmail(String email) { - List findMembers = memberRepository.findMemberByEmail(email); - if (!findMembers.isEmpty()) { - throw new IllegalArgumentException("이미 존재하는 이메일 입니다"); - } + memberRepository.findMemberByEmail(email).orElseThrow(() -> new IllegalArgumentException("이미 존재하는 이메일 입니다")); } @Transactional @@ -37,4 +35,14 @@ public void linkMember(Long id, MemberProfile profile) { Member member = memberRepository.findById(id).orElseThrow(() -> new IllegalStateException("존재하지 않는 회원입니다")); member.registerMemberProfile(profile); } + + public MemberProfile findMemberProfileByMemberId(Long memberId) { + return memberProfileRepository.findMemberProfileByMemberId(memberId) + .orElseThrow(() -> new IllegalStateException("프로필이 없는 유저를 조회하였습니다.")); + + } + + public boolean isNewMember(String email) { + return memberRepository.findMemberByEmail(email).isEmpty(); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java index 92f42bc..7d1c293 100644 --- a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java @@ -2,23 +2,28 @@ import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; import com.stcom.smartmealtable.domain.food.FoodCategory; import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberGroup; import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.exception.PasswordPolicyException; import com.stcom.smartmealtable.infrastructure.AddressApiService; -import com.stcom.smartmealtable.infrastructure.dto.AddressRequestDto; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.security.JwtAuthorization; import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.BudgetService; +import com.stcom.smartmealtable.service.FoodPreferenceService; import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; +import com.stcom.smartmealtable.service.SocialAccountService; import com.stcom.smartmealtable.web.dto.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; @@ -43,6 +48,9 @@ public class MemberController { private final MemberService memberService; private final JwtTokenService jwtTokenService; private final AddressApiService addressApiService; + private final BudgetService budgetService; + private final SocialAccountService socialAccountService; + private final FoodPreferenceService foodPreferenceService; @GetMapping("/email/check") public ResponseEntity> checkEmail(@Email @RequestParam String email) { @@ -82,8 +90,40 @@ public ApiResponse createMemberProfile(@JwtAuthorization Member member, return ApiResponse.createSuccessWithNoContent(); } + @GetMapping("/profile") + public ApiResponse memberProfile(@JwtAuthorization Member member) { + MemberProfile memberProfile = memberService.findMemberProfileByMemberId(member.getId()); + DailyBudget dailyBudget = budgetService.findRecentDailyBudgetByMemberId(member.getId()); + MonthlyBudget monthlyBudget = budgetService.findRecentMonthlyBudgetByMemberId(member.getId()); + List providers = socialAccountService.findAllProviders(member.getId()); + List foodCategories = foodPreferenceService.findPreferredFoodCategories(member); + MemberProfileResponse memberProfileResponse = + buildMemberProfileResponse(member, memberProfile, foodCategories, dailyBudget, monthlyBudget); + return ApiResponse.createSuccess(memberProfileResponse); + } + + private MemberProfileResponse buildMemberProfileResponse(Member member, MemberProfile memberProfile, + List foodCategories, DailyBudget dailyBudget, + MonthlyBudget monthlyBudget) { + return MemberProfileResponse.builder() + .memberGroup(memberProfile.getMemberGroup()) + .groupName(memberProfile.getGroupName()) + .email(member.getEmail()) + .nickName(memberProfile.getNickName()) + .preferredFoodCategory(foodCategories) + .dailyLimitAmount(dailyBudget.getLimit().longValue()) + .dailyAvailableAmount(dailyBudget.getAvailableAmount().longValue()) + .monthlyLimitAmount(monthlyBudget.getLimit().longValue()) + .monthlyAvailableAmount(monthlyBudget.getAvailableAmount().longValue()) + .addressList(memberProfile.getAddressHistory().stream() + .map(a -> a.getRoadAddress() + a.getDetailAddress()) + .toList()) + .build(); + } + private Address getAddressFromApiService(AddressRequest request) { - AddressRequestDto dto = new AddressRequestDto(request.getRoadAddress(), AddressType.HOME, "집", + com.stcom.smartmealtable.infrastructure.dto.AddressRequest dto = new com.stcom.smartmealtable.infrastructure.dto.AddressRequest( + request.getRoadAddress(), AddressType.HOME, "집", request.getDetailAddress()); return addressApiService.createAddressFromRequest(dto); } @@ -103,7 +143,7 @@ static class CreateMemberRequest { @NoArgsConstructor static class CreateMemberProfileRequest { private MemberGroup groupType; - private Long groupCode; + private String groupName; private AddressRequest homeAddress; private List foodPreference; private List hateFoods; @@ -113,7 +153,7 @@ static class CreateMemberProfileRequest { public MemberProfile toEntity() { return MemberProfile.builder() .memberGroup(groupType) - .groupCode(groupCode) + .groupName(groupName) .nickName("test") .addressHistory(new ArrayList<>()) .build(); @@ -129,5 +169,37 @@ static class AddressRequest { private String detailAddress; } + @Data + @NoArgsConstructor + static class MemberProfileResponse { + private String nickName; + private List preferredFoodCategory; + private String email; + private List addressList; + private MemberGroup memberGroup; + private String groupName; + private Long dailyAvailableAmount; + private Long dailyLimitAmount; + private Long monthlyAvailableAmount; + private Long monthlyLimitAmount; + + @Builder + public MemberProfileResponse(String nickName, List preferredFoodCategory, String email, + List addressList, MemberGroup memberGroup, String groupName, + Long dailyAvailableAmount, + Long dailyLimitAmount, Long monthlyAvailableAmount, Long monthlyLimitAmount) { + this.nickName = nickName; + this.preferredFoodCategory = preferredFoodCategory; + this.email = email; + this.addressList = addressList; + this.memberGroup = memberGroup; + this.groupName = groupName; + this.dailyAvailableAmount = dailyAvailableAmount; + this.dailyLimitAmount = dailyLimitAmount; + this.monthlyAvailableAmount = monthlyAvailableAmount; + this.monthlyLimitAmount = monthlyLimitAmount; + } + } + } From 3515a5fd3702fa87127b54f09540b8d2123eccf6 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 23:34:31 +0900 Subject: [PATCH 071/120] =?UTF-8?q?refactor:=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/domain/member/MemberProfile.java | 8 +++++--- .../stcom/smartmealtable/domain/social/SocialAccount.java | 2 ++ .../smartmealtable/infrastructure/AddressApiService.java | 6 +++--- .../infrastructure/{social => }/SocialAuthService.java | 7 +++++-- .../dto/{AddressRequestDto.java => AddressRequest.java} | 2 +- .../token => infrastructure/dto}/JwtTokenResponseDto.java | 2 +- .../infrastructure/social/SocialHttpMessage.java | 2 +- .../stcom/smartmealtable/security/JwtTokenService.java | 2 +- 8 files changed, 19 insertions(+), 12 deletions(-) rename src/main/java/com/stcom/smartmealtable/infrastructure/{social => }/SocialAuthService.java (79%) rename src/main/java/com/stcom/smartmealtable/infrastructure/dto/{AddressRequestDto.java => AddressRequest.java} (90%) rename src/main/java/com/stcom/smartmealtable/{service/dto/token => infrastructure/dto}/JwtTokenResponseDto.java (90%) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 217734d..d4cb9c9 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -16,10 +16,12 @@ import java.util.ArrayList; import java.util.List; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor +@Getter public class MemberProfile extends BaseTimeEntity { @Id @@ -33,7 +35,7 @@ public class MemberProfile extends BaseTimeEntity { @Enumerated(EnumType.STRING) private MemberGroup memberGroup; - private Long groupCode; + private String groupName; private String nickName; @@ -42,11 +44,11 @@ public class MemberProfile extends BaseTimeEntity { private List
addressHistory = new ArrayList<>(); @Builder - public MemberProfile(Member member, MemberGroup memberGroup, Long groupCode, String nickName, + public MemberProfile(Member member, MemberGroup memberGroup, String groupName, String nickName, List
addressHistory) { this.member = member; this.memberGroup = memberGroup; - this.groupCode = groupCode; + this.groupName = groupName; this.nickName = nickName; this.addressHistory = addressHistory; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index 12d1d78..deb10f2 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -12,10 +12,12 @@ import jakarta.persistence.ManyToOne; import java.time.LocalDateTime; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor +@Getter public class SocialAccount extends BaseTimeEntity { @Id diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java index 85a7478..07b7375 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.infrastructure.dto.AddressRequestDto; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; @@ -20,7 +20,7 @@ public class AddressApiService { private final RestClient client = RestClient.create(); - public Address createAddressFromRequest(AddressRequestDto requestDto) { + public Address createAddressFromRequest(AddressRequest requestDto) { AddressSearchResponse addressSearchResponse = client.get() .uri(uriBuilder -> uriBuilder .scheme("https") @@ -35,7 +35,7 @@ public Address createAddressFromRequest(AddressRequestDto requestDto) { if (addressSearchResponse.getMeta().getTotalCount() >= 2) { throw new IllegalArgumentException("주소가 모호합니다. 정확한 주소를 입력하세요"); } - + return Address.builder() .type(requestDto.getAddressType()) .alias(requestDto.getAlias()) diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java b/src/main/java/com/stcom/smartmealtable/infrastructure/SocialAuthService.java similarity index 79% rename from src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/SocialAuthService.java index cb11aef..3bd8937 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialAuthService.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/SocialAuthService.java @@ -1,9 +1,12 @@ -package com.stcom.smartmealtable.infrastructure.social; +package com.stcom.smartmealtable.infrastructure; import static com.stcom.smartmealtable.infrastructure.social.SocialConst.GOOGLE; import static com.stcom.smartmealtable.infrastructure.social.SocialConst.KAKAO; -import com.stcom.smartmealtable.service.dto.token.TokenDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.infrastructure.social.GoogleHttpMessage; +import com.stcom.smartmealtable.infrastructure.social.KakaoHttpMessage; +import com.stcom.smartmealtable.infrastructure.social.SocialHttpMessage; import jakarta.annotation.PostConstruct; import jakarta.validation.constraints.NotEmpty; import java.util.HashMap; diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java similarity index 90% rename from src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java index b6bd9af..d97cbdb 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequestDto.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java @@ -6,7 +6,7 @@ @Data @AllArgsConstructor -public class AddressRequestDto { +public class AddressRequest { private String roadAddress; diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/JwtTokenResponseDto.java similarity index 90% rename from src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java rename to src/main/java/com/stcom/smartmealtable/infrastructure/dto/JwtTokenResponseDto.java index 8ab81a1..bc674ca 100644 --- a/src/main/java/com/stcom/smartmealtable/service/dto/token/JwtTokenResponseDto.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/JwtTokenResponseDto.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.service.dto.token; +package com.stcom.smartmealtable.infrastructure.dto; import lombok.Data; import lombok.ToString; diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java index b20d7b2..fec7a22 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/social/SocialHttpMessage.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.infrastructure.social; -import com.stcom.smartmealtable.service.dto.token.TokenDto; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient.RequestBodySpec; import org.springframework.web.client.RestClient.ResponseSpec; diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java index 4127570..f66996d 100644 --- a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java +++ b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java @@ -1,8 +1,8 @@ package com.stcom.smartmealtable.security; import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.repository.MemberRepository; -import com.stcom.smartmealtable.service.dto.token.JwtTokenResponseDto; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; From 47c1acaa9f395ce039402dc95ec5839fff7d4484 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Mon, 19 May 2025 23:34:45 +0900 Subject: [PATCH 072/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/service/LoginService.java | 23 ++++++++ .../web/auth/LoginController.java | 53 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/service/LoginService.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java diff --git a/src/main/java/com/stcom/smartmealtable/service/LoginService.java b/src/main/java/com/stcom/smartmealtable/service/LoginService.java new file mode 100644 index 0000000..c95d701 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/LoginService.java @@ -0,0 +1,23 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final MemberRepository memberRepository; + + public Member login(String email, String password) throws PasswordFailedExceededException { + Member findMember = memberRepository.findMemberByEmail(email) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + if (!findMember.isMatchedPassword(password)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다"); + } + return findMember; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java new file mode 100644 index 0000000..9f56a85 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java @@ -0,0 +1,53 @@ +package com.stcom.smartmealtable.web.auth; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.security.JwtAuthorization; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.LoginService; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@Slf4j +public class LoginController { + + private final LoginService loginService; + private final JwtTokenService jwtTokenService; + + @PostMapping("/api/v1/login") + public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginRequest request) + throws PasswordFailedExceededException { + if (member == null) { + member = loginService.login(request.getEmail(), request.getPassword()); + } + + JwtTokenResponseDto tokenResponseDto = jwtTokenService.createTokenDto(member.getId()); + if (member.getMemberProfile() == null) { + tokenResponseDto.setNewUser(true); + } + log.info("response = {}", tokenResponseDto); + return ApiResponse.createSuccess(tokenResponseDto); + } + + + @Data + static class LoginRequest { + + @NotEmpty + @Email + private String email; + + @NotEmpty + private String password; + } +} From 05b531d6b0e83d56cfd8a3eecfc5c2c021602d77 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 20 May 2025 15:55:20 +0900 Subject: [PATCH 073/120] =?UTF-8?q?chore:=20data=20redis=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index fdb53aa..73c2032 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화/역직렬화에 Jackson 사용 From 0a86520e5a29ec1e398a74b6f132c6a751fb67e6 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 20 May 2025 15:55:44 +0900 Subject: [PATCH 074/120] =?UTF-8?q?feat:=20logout=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JWT 블랙리스트 추가 방식으로 구현 --- .../infrastructure/config/RedisConfig.java | 33 +++++++++++ .../security/JwtBlacklistService.java | 55 +++++++++++++++++++ .../security/JwtTokenService.java | 29 +++++----- .../JwtAuthorizationArgumentResolver.java | 6 +- .../web/auth/LoginController.java | 12 ++++ 5 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/infrastructure/config/RedisConfig.java create mode 100644 src/main/java/com/stcom/smartmealtable/security/JwtBlacklistService.java diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/config/RedisConfig.java b/src/main/java/com/stcom/smartmealtable/infrastructure/config/RedisConfig.java new file mode 100644 index 0000000..42e30cc --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/config/RedisConfig.java @@ -0,0 +1,33 @@ +package com.stcom.smartmealtable.infrastructure.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host:localhost}") + private String host; + + @Value("${spring.data.redis.port:6379}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtBlacklistService.java b/src/main/java/com/stcom/smartmealtable/security/JwtBlacklistService.java new file mode 100644 index 0000000..655d7d7 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/security/JwtBlacklistService.java @@ -0,0 +1,55 @@ +package com.stcom.smartmealtable.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtBlacklistService { + + private final RedisTemplate redisTemplate; + + @Value("${jwt.secret}") + private String jwtSecret; + + private static final String BLACKLIST_PREFIX = "jwt:blacklist:"; + + + public void addToBlacklist(String token) { + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); + } + + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token) + .getBody(); + + Date expiration = claims.getExpiration(); + long ttl = expiration.getTime() - System.currentTimeMillis(); + + String key = BLACKLIST_PREFIX + token; + redisTemplate.opsForValue().set(key, "blacklisted", ttl, TimeUnit.MILLISECONDS); + log.info("토큰이 블랙리스트에 추가되었습니다. 만료 시간: {}", expiration); + } + + public boolean isBlacklisted(String token) { + if (token != null && token.startsWith("Bearer ")) { + token = token.substring(7); + } + + String key = BLACKLIST_PREFIX + token; + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java index f66996d..5d557d6 100644 --- a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java +++ b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java @@ -19,6 +19,7 @@ public class JwtTokenService { private final MemberRepository memberRepository; + private final JwtBlacklistService jwtBlacklistService; @Value("${jwt.secret}") private String jwtSecret; @@ -72,23 +73,21 @@ public String extractMemberIdFromRefreshToken(String refreshToken) { } } - public boolean validateToken(String token) { - try { - // Bearer 접두사 제거 - if (token.startsWith("Bearer ")) { - token = token.substring(7); - } - - // 토큰 검증 - Jwts.parserBuilder() - .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) - .build() - .parseClaimsJws(token); + public void validateToken(String token) { + // Bearer 접두사 제거 + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } - return true; - } catch (Exception e) { - return false; + if (jwtBlacklistService.isBlacklisted(token)) { + throw new IllegalArgumentException("블랙리스트에 추가된 토큰으로 접근하였습니다"); } + + // 토큰 검증 + Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token); } public Member getClaim(String token) { diff --git a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java index ef9e7ce..13b9d6e 100644 --- a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java +++ b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java @@ -33,10 +33,8 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m if (token != null && !token.trim().equals("")) { // 토큰 있을 경우 검증 - if (jwtTokenService.validateToken(token)) { - // 검증 후 MemberProfile 리턴 - return jwtTokenService.getClaim(token); - } + jwtTokenService.validateToken(token); + return jwtTokenService.getClaim(token); } } diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java index 9f56a85..ee1fd03 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java @@ -4,9 +4,11 @@ import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.security.JwtAuthorization; +import com.stcom.smartmealtable.security.JwtBlacklistService; import com.stcom.smartmealtable.security.JwtTokenService; import com.stcom.smartmealtable.service.LoginService; import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotEmpty; import lombok.Data; @@ -23,6 +25,7 @@ public class LoginController { private final LoginService loginService; private final JwtTokenService jwtTokenService; + private final JwtBlacklistService jwtBlacklistService; @PostMapping("/api/v1/login") public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginRequest request) @@ -39,6 +42,15 @@ public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginR return ApiResponse.createSuccess(tokenResponseDto); } + @PostMapping("/api/v1/logout") + public ApiResponse logout(HttpServletRequest request) { + String jwt = request.getHeader("Authorization"); + if (jwt.isBlank()) { + return ApiResponse.createError("인증 토큰이 없습니다"); + } + jwtBlacklistService.addToBlacklist(jwt); + return ApiResponse.createSuccessWithNoContent(); + } @Data static class LoginRequest { From eefc8970bd7f026397c0e2cbaffbeb18604cbfe2 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 20 May 2025 15:57:31 +0900 Subject: [PATCH 075/120] =?UTF-8?q?fix:=20Api=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EC=99=80=20=EB=8B=A4=EB=A5=B8=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/stcom/smartmealtable/web/auth/LoginController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java index ee1fd03..8493470 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java +++ b/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java @@ -16,18 +16,20 @@ import lombok.extern.slf4j.Slf4j; 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; @RestController @RequiredArgsConstructor @Slf4j +@RequestMapping("/api/v1/auth") public class LoginController { private final LoginService loginService; private final JwtTokenService jwtTokenService; private final JwtBlacklistService jwtBlacklistService; - @PostMapping("/api/v1/login") + @PostMapping("/login") public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginRequest request) throws PasswordFailedExceededException { if (member == null) { @@ -42,7 +44,7 @@ public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginR return ApiResponse.createSuccess(tokenResponseDto); } - @PostMapping("/api/v1/logout") + @PostMapping("/logout") public ApiResponse logout(HttpServletRequest request) { String jwt = request.getHeader("Authorization"); if (jwt.isBlank()) { From 48a89dc63aa6a4798f03bb53e05966488dd42282 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Tue, 20 May 2025 16:07:39 +0900 Subject: [PATCH 076/120] =?UTF-8?q?fix:=20Api=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EC=99=80=20=EB=8B=A4=EB=A5=B8=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/stcom/smartmealtable/web/member/MemberController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java index 7d1c293..3091cca 100644 --- a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java @@ -90,7 +90,7 @@ public ApiResponse createMemberProfile(@JwtAuthorization Member member, return ApiResponse.createSuccessWithNoContent(); } - @GetMapping("/profile") + @GetMapping("/profile/me") public ApiResponse memberProfile(@JwtAuthorization Member member) { MemberProfile memberProfile = memberService.findMemberProfileByMemberId(member.getId()); DailyBudget dailyBudget = budgetService.findRecentDailyBudgetByMemberId(member.getId()); From 365f3204e6f1395cf406f6947e014c7212263988 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 21:35:08 +0900 Subject: [PATCH 077/120] =?UTF-8?q?refactor:=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=84=A4=EA=B3=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Address를 값타입으로 변경 2. 별도의 엔티티를 선언하여 AddressEntity로 승급 --- .../domain/Address/Address.java | 13 +----- .../domain/Address/AddressEntity.java | 46 +++++++++++++++++++ .../domain/Address/AddressType.java | 2 +- .../repository/AddressRepository.java | 1 + 4 files changed, 50 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java index a58fd7b..c1fa98f 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java @@ -1,26 +1,17 @@ package com.stcom.smartmealtable.domain.Address; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; +import jakarta.persistence.Embeddable; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -@Entity @Getter +@Embeddable @NoArgsConstructor public class Address { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "address_id") - private Long id; - private String lotNumberAddress; private String roadAddress; diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java new file mode 100644 index 0000000..dabca3d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java @@ -0,0 +1,46 @@ +package com.stcom.smartmealtable.domain.Address; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@Getter +@Table(name = "address") +public class AddressEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "address_id") + private Long id; + + @Embedded + private Address address; + + private boolean primary = false; + + public AddressEntity(Address address) { + this.address = address; + } + + public void markPrimary() { + this.primary = true; + } + + public void unmarkPrimary() { + this.primary = false; + } + + public boolean isPrimaryAddress() { + return primary; + } + + +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java index 6784561..33f0de4 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressType.java @@ -1,5 +1,5 @@ package com.stcom.smartmealtable.domain.Address; public enum AddressType { - HOME, SCHOOL, OFFICE + HOME, SCHOOL, OFFICE, ETC } diff --git a/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java b/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java index 8daa45a..a604c1d 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface AddressRepository extends JpaRepository { + } From 6caa0d1143158d3df0ea91f22ef8b492e298536c Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 21:37:48 +0900 Subject: [PATCH 078/120] =?UTF-8?q?refactor:=20Aggregate=20Root=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Auth Context와 Profile Context로 분리 2. Auth Aggregate Root -> Member 3. Profile Aggregate Root -> MemberProfile --- .../smartmealtable/domain/Budget/Budget.java | 10 ++-- .../domain/Budget/DailyBudget.java | 6 +- .../domain/Budget/MonthlyBudget.java | 6 +- .../domain/food/FoodPreference.java | 10 ++-- .../smartmealtable/domain/member/Member.java | 16 +++--- .../domain/member/MemberGroup.java | 5 -- .../domain/member/MemberProfile.java | 55 ++++++++++++------- .../domain/social/SocialAccount.java | 4 ++ 8 files changed, 62 insertions(+), 50 deletions(-) delete mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index a3f0863..d0995a7 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -1,7 +1,7 @@ package com.stcom.smartmealtable.domain.Budget; import com.stcom.smartmealtable.domain.common.BaseTimeEntity; -import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Entity; @@ -29,16 +29,16 @@ public abstract class Budget extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + @JoinColumn(name = "member_profile_id") + private MemberProfile memberProfile; private BigDecimal spendAmount = BigDecimal.ZERO; @Column(name = "budget_limit") private BigDecimal limit; - protected Budget(Member member, BigDecimal limit) { - this.member = member; + protected Budget(MemberProfile memberProfile, BigDecimal limit) { + this.memberProfile = memberProfile; this.limit = limit; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java index 4651133..6477ae1 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/DailyBudget.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.Budget; -import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; import jakarta.persistence.Column; import jakarta.persistence.Entity; import java.math.BigDecimal; @@ -13,9 +13,9 @@ @NoArgsConstructor public class DailyBudget extends Budget { - public DailyBudget(Member member, BigDecimal limit, + public DailyBudget(MemberProfile memberProfile, BigDecimal limit, LocalDate date) { - super(member, limit); + super(memberProfile, limit); this.date = date; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java index f64924b..beb87b7 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudget.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.domain.Budget; -import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.infrastructure.persistence.YearMonthConverter; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -15,9 +15,9 @@ @NoArgsConstructor public class MonthlyBudget extends Budget { - public MonthlyBudget(Member member, BigDecimal limit, + public MonthlyBudget(MemberProfile memberProfile, BigDecimal limit, YearMonth yearMonth) { - super(member, limit); + super(memberProfile, limit); this.yearMonth = yearMonth; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java index a6657e1..665f466 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java @@ -1,7 +1,7 @@ package com.stcom.smartmealtable.domain.food; import com.stcom.smartmealtable.domain.common.BaseTimeEntity; -import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -27,8 +27,8 @@ public class FoodPreference extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; + @JoinColumn(name = "member_profile_id") + private MemberProfile memberProfile; @Enumerated(EnumType.STRING) @Column(name = "category") @@ -39,8 +39,8 @@ public class FoodPreference extends BaseTimeEntity { private Double weight; @Builder - public FoodPreference(Member member, FoodCategory category, boolean isPreferred, Double weight) { - this.member = member; + public FoodPreference(MemberProfile memberProfile, FoodCategory category, boolean isPreferred, Double weight) { + this.memberProfile = memberProfile; this.category = category; this.isPreferred = isPreferred; this.weight = weight; diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index ba6d121..175c703 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -3,15 +3,12 @@ import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.exception.PasswordPolicyException; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.validation.constraints.Email; import lombok.Builder; @@ -36,11 +33,11 @@ public class Member extends BaseTimeEntity { private String fullName; + // TODO: 이메일 인증 기능 구현해야함 private boolean isEmailVerified = true; - @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL) - @JoinColumn(name = "member_profile_id") + @OneToOne(mappedBy = "member") private MemberProfile memberProfile; public Member(String email) { @@ -54,9 +51,8 @@ public Member(String fullName, String email, String rawPassword) throws Password this.password = new MemberPassword(rawPassword); } - public void registerMemberProfile(MemberProfile profile) { - memberProfile = profile; - profile.linkMemberAuth(this); + protected void linkMemberProfile(MemberProfile profile) { + this.memberProfile = profile; } public void changePassword(String rawOldPassword, String rawNewPassword) @@ -79,5 +75,9 @@ public void verifyEmail() { this.isEmailVerified = true; } + public boolean isProfileRegistered() { + return memberProfile == null; + } + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java deleted file mode 100644 index 75f1486..0000000 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberGroup.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.stcom.smartmealtable.domain.member; - -public enum MemberGroup { - UNIVERSITY, STUDENT, NONE, ADMIN -} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index d4cb9c9..ec6c4d9 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -1,16 +1,18 @@ package com.stcom.smartmealtable.domain.member; import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressEntity; import com.stcom.smartmealtable.domain.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.group.Group; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; import java.util.ArrayList; @@ -29,31 +31,45 @@ public class MemberProfile extends BaseTimeEntity { @Column(name = "member_profile_id") private Long id; - @OneToOne(mappedBy = "memberProfile", orphanRemoval = true) + @OneToOne(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL) + @JoinColumn(name = "member_id") private Member member; - @Enumerated(EnumType.STRING) - private MemberGroup memberGroup; - - private String groupName; - private String nickName; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "member_id") - private List
addressHistory = new ArrayList<>(); + private List addressHistory = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id") + private Group group; @Builder - public MemberProfile(Member member, MemberGroup memberGroup, String groupName, String nickName, - List
addressHistory) { - this.member = member; - this.memberGroup = memberGroup; - this.groupName = groupName; + public MemberProfile(Member member, String nickName, List addressHistory, Group group) { + linkMember(member); this.nickName = nickName; this.addressHistory = addressHistory; + this.group = group; + } + + public void addAddress(Address address) { + addressHistory.add(new AddressEntity(address)); } - protected void linkMemberAuth(Member member) { + public void removeAddress(AddressEntity addressEntity) { + addressHistory.remove(addressEntity); + } + + public AddressEntity findPrimaryAddress() { + return addressHistory.stream() + .filter(AddressEntity::isPrimaryAddress) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Primary Address가 없습니다")); + } + + public void linkMember(Member member) { + member.linkMemberProfile(this); this.member = member; } @@ -64,11 +80,8 @@ public void changeNickName(String newNickName) { this.nickName = newNickName; } - public void addAddress(Address address) { - addressHistory.add(address); - } - - public void removeAddress(Address address) { - addressHistory.remove(address); + public void setPrimaryAddress(AddressEntity target) { + addressHistory.forEach(AddressEntity::unmarkPrimary); + target.markPrimary(); } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java index deb10f2..8c5cc14 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java +++ b/src/main/java/com/stcom/smartmealtable/domain/social/SocialAccount.java @@ -58,4 +58,8 @@ public void updateToken(String accessToken, String refreshToken, LocalDateTime t this.refreshToken = refreshToken; this.tokenExpiresAt = tokenExpiresAt; } + + public boolean isProfileRegistered() { + return member.isProfileRegistered(); + } } From cb273dccab489aff18c139cfe90879b37730a0cf Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 21:41:46 +0900 Subject: [PATCH 079/120] =?UTF-8?q?refactor:=20Jwt=20=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Interceptor가 JWT 유효성 검증 2. ArgumentResolver는 엔티티가 아닌 Dto를 주입 --- .../repository/MemberProfileRepository.java | 7 + .../repository/MemberRepository.java | 2 +- .../repository/SocialAccountRepository.java | 19 ++ .../security/JwtTokenService.java | 76 +++---- .../smartmealtable/service/LoginService.java | 49 ++++- .../smartmealtable/service/MemberService.java | 37 ++-- .../service/SocialAccountService.java | 7 +- .../service/dto/AuthResultDto.java | 12 + .../smartmealtable/service/dto/MemberDto.java | 24 ++ .../stcom/smartmealtable/web/WebConfig.java | 15 +- .../JwtAuthorizationArgumentResolver.java | 44 ---- .../argumentresolver/UserContext.java} | 4 +- .../UserContextArgumentResolver.java | 66 ++++++ .../{auth => controller}/LoginController.java | 26 +-- .../web/controller/MemberController.java | 109 ++++++++++ .../OAuth2Controller.java | 44 ++-- .../JwtAuthenticationInterceptor.java | 51 +++++ .../web/member/MemberController.java | 205 ------------------ 18 files changed, 427 insertions(+), 370 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/service/dto/AuthResultDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/dto/MemberDto.java delete mode 100644 src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java rename src/main/java/com/stcom/smartmealtable/{security/JwtAuthorization.java => web/argumentresolver/UserContext.java} (80%) create mode 100644 src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolver.java rename src/main/java/com/stcom/smartmealtable/web/{auth => controller}/LoginController.java (67%) create mode 100644 src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java rename src/main/java/com/stcom/smartmealtable/web/{auth => controller}/OAuth2Controller.java (55%) create mode 100644 src/main/java/com/stcom/smartmealtable/web/interceptor/JwtAuthenticationInterceptor.java delete mode 100644 src/main/java/com/stcom/smartmealtable/web/member/MemberController.java diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java index 0058910..1a22927 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberProfileRepository.java @@ -1,7 +1,9 @@ package com.stcom.smartmealtable.repository; +import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberProfile; import java.util.Optional; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,4 +12,9 @@ public interface MemberProfileRepository extends JpaRepository findMemberProfileByMemberId(@Param("memberId") Long memberId); + + void deleteMemberProfileByMember(Member member); + + @EntityGraph(attributePaths = {"member", "addressHistory, group"}) + Optional findMemberProfileEntityGraphById(Long id); } diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java index ff8795e..c424179 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberRepository.java @@ -7,5 +7,5 @@ public interface MemberRepository extends JpaRepository { - Optional findMemberByEmail(@Email String email); + Optional findByEmail(@Email String email); } diff --git a/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java index ebb10cc..1db8947 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/SocialAccountRepository.java @@ -1,5 +1,6 @@ package com.stcom.smartmealtable.repository; +import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.social.SocialAccount; import java.util.List; import java.util.Optional; @@ -18,4 +19,22 @@ Optional findMemberIdByProviderAndProviderUserId( ); List findAllByMemberId(Long memberId); + + void deleteSocialAccountByMember(Member member); + + /** + * 소셜 계정(provider, providerUserId)에 연결된 MemberProfile의 ID만 조회 + */ + @Query(""" + select p.id + from SocialAccount sa + join sa.member m + join m.memberProfile p + where sa.provider = :provider + and sa.providerUserId = :providerUserId + """) + Optional findProfileIdByProviderAndProviderUserId( + @Param("provider") String provider, + @Param("providerUserId") String providerUserId + ); } diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java index 5d557d6..095335e 100644 --- a/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java +++ b/src/main/java/com/stcom/smartmealtable/security/JwtTokenService.java @@ -3,6 +3,7 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.repository.MemberRepository; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -24,24 +25,27 @@ public class JwtTokenService { @Value("${jwt.secret}") private String jwtSecret; - public String createAccessToken(Long memberId) { - return createToken(String.valueOf(memberId), 1000 * 60 * 60); + public String createAccessToken(Long memberId, Long profileId) { + return createToken(String.valueOf(memberId), profileId, 1000 * 60 * 60); } - public String createAccessToken(String memberId) { - return createToken(memberId, 1000 * 60 * 60); + public String createRefreshToken(Long memberId, Long profileId) { + return createToken(String.valueOf(memberId), profileId, 1000 * 60 * 60 * 24 * 14); } - public String createRefreshToken(Long memberId) { - return createToken(String.valueOf(memberId), 1000 * 60 * 60 * 24 * 14); - } - - private String createToken(String memberId, long expireTime) { + private String createToken(String memberId, Long profileId, long expireTime) { Date now = new Date(); Date expiration = new Date(now.getTime() + expireTime); + Member member = memberRepository.findById(Long.parseLong(memberId)) + .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다")); + Map claims = new HashMap<>(); claims.put("memberId", memberId); + claims.put("email", member.getEmail()); + if (profileId != null) { + claims.put("profileId", String.valueOf(profileId)); + } return Jwts.builder() .setClaims(claims) @@ -51,28 +55,15 @@ private String createToken(String memberId, long expireTime) { .compact(); } - public JwtTokenResponseDto createTokenDto(Long memberId) { + public JwtTokenResponseDto createTokenDto(Long memberId, Long profileId) { return new JwtTokenResponseDto( - createAccessToken(memberId), - createRefreshToken(memberId), + createAccessToken(memberId, profileId), + createRefreshToken(memberId, profileId), 3600, - "Bearar" + "Bearer" ); } - public String extractMemberIdFromRefreshToken(String refreshToken) { - try { - return Jwts.parserBuilder() - .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) - .build() - .parseClaimsJws(refreshToken) - .getBody() - .get("memberId", String.class); - } catch (Exception e) { - throw new RuntimeException("유효하지 않은 리프레시 토큰입니다"); - } - } - public void validateToken(String token) { // Bearer 접두사 제거 if (token.startsWith("Bearer ")) { @@ -90,29 +81,16 @@ public void validateToken(String token) { .parseClaimsJws(token); } - public Member getClaim(String token) { - try { - // Bearer 접두사 제거 - if (token.startsWith("Bearer ")) { - token = token.substring(7); - } - - String memberIdStr = Jwts.parserBuilder() - .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) - .build() - .parseClaimsJws(token) - .getBody() - .get("memberId", String.class); - - Long memberId = Long.parseLong(memberIdStr); - - // 회원 정보 조회 - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다")); - - return member; - } catch (Exception e) { - throw new RuntimeException("토큰 처리 중 오류가 발생했습니다: " + e.getMessage()); + public Claims extractClaims(String token) { + // Bearer 접두사 제거 + if (token.startsWith("Bearer ")) { + token = token.substring(7); } + + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token) + .getBody(); } } diff --git a/src/main/java/com/stcom/smartmealtable/service/LoginService.java b/src/main/java/com/stcom/smartmealtable/service/LoginService.java index c95d701..f1f1c1f 100644 --- a/src/main/java/com/stcom/smartmealtable/service/LoginService.java +++ b/src/main/java/com/stcom/smartmealtable/service/LoginService.java @@ -1,23 +1,66 @@ package com.stcom.smartmealtable.service; import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.social.SocialAccount; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class LoginService { private final MemberRepository memberRepository; + private final SocialAccountRepository socialAccountRepository; - public Member login(String email, String password) throws PasswordFailedExceededException { - Member findMember = memberRepository.findMemberByEmail(email) + public AuthResultDto loginWithEmail(String email, String password) throws PasswordFailedExceededException { + Member findMember = memberRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); if (!findMember.isMatchedPassword(password)) { throw new IllegalArgumentException("비밀번호가 일치하지 않습니다"); } - return findMember; + boolean newUser = findMember.isProfileRegistered(); + Long profileId = newUser ? null : findMember.getMemberProfile().getId(); + return new AuthResultDto(findMember.getId(), profileId, newUser); } + + @Transactional + public AuthResultDto socialLogin(TokenDto token) { + Member member = memberRepository.findByEmail(token.getEmail()) + .orElseGet(() -> memberRepository.save(new Member(token.getEmail()))); + + SocialAccount sa = socialAccountRepository.findByProviderAndProviderUserId( + token.getProvider(), token.getProviderUserId()) + .map(existing -> { + existing.updateToken( + token.getAccessToken(), + token.getRefreshToken(), + LocalDateTime.now().plusSeconds(token.getExpiresIn())); + return existing; + }) + .orElseGet(() -> socialAccountRepository.save( + SocialAccount.builder() + .member(member) + .provider(token.getProvider()) + .providerUserId(token.getProviderUserId()) + .tokenType(token.getTokenType()) + .accessToken(token.getAccessToken()) + .refreshToken(token.getRefreshToken()) + .tokenExpiresAt(LocalDateTime.now().plusSeconds(token.getExpiresIn())) + .build() + )); + + Long profileId = socialAccountRepository + .findProfileIdByProviderAndProviderUserId(token.getProvider(), token.getProviderUserId()) + .orElse(null); + boolean newUser = (profileId == null); + return new AuthResultDto(member.getId(), profileId, newUser); + } + } diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index 258cf29..8212e4d 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -1,9 +1,12 @@ package com.stcom.smartmealtable.service; import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.repository.AddressRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,9 +17,11 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberProfileRepository memberProfileRepository; + private final SocialAccountRepository socialAccountRepository; + private final AddressRepository addressRepository; public void validateDuplicatedEmail(String email) { - memberRepository.findMemberByEmail(email).orElseThrow(() -> new IllegalArgumentException("이미 존재하는 이메일 입니다")); + memberRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("이미 존재하는 이메일 입니다")); } @Transactional @@ -24,25 +29,29 @@ public void saveMember(Member member) { memberRepository.save(member); } + public Member findMemberByMemberId(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); + } + public void checkPasswordDoubly(String password, String confirmPassword) { if (!password.equals(confirmPassword)) { throw new IllegalArgumentException("비밀번호가 일치하지 않습니다"); } } - @Transactional - public void linkMember(Long id, MemberProfile profile) { - Member member = memberRepository.findById(id).orElseThrow(() -> new IllegalStateException("존재하지 않는 회원입니다")); - member.registerMemberProfile(profile); - } - - public MemberProfile findMemberProfileByMemberId(Long memberId) { - return memberProfileRepository.findMemberProfileByMemberId(memberId) - .orElseThrow(() -> new IllegalStateException("프로필이 없는 유저를 조회하였습니다.")); - + public void changePassword(Long memberId, String originPassword, String newPassword) + throws PasswordFailedExceededException, PasswordPolicyException { + Member findMember = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalStateException("회원이 존재하지 않습니다")); + findMember.changePassword(originPassword, newPassword); } - public boolean isNewMember(String email) { - return memberRepository.findMemberByEmail(email).isEmpty(); + public void deleteByMemberId(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalStateException("회원이 존재하지 않습니다")); + memberProfileRepository.deleteMemberProfileByMember(member); + socialAccountRepository.deleteSocialAccountByMember(member); + memberRepository.delete(member); } } diff --git a/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java index 552037a..ce16baf 100644 --- a/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java +++ b/src/main/java/com/stcom/smartmealtable/service/SocialAccountService.java @@ -38,7 +38,7 @@ public void createNewMemberAndLinkSocialAccount(TokenDto tokenDto) { @Transactional public void linkSocialAccount(TokenDto tokenDto) { - Member member = memberRepository.findMemberByEmail(tokenDto.getEmail()) + Member member = memberRepository.findByEmail(tokenDto.getEmail()) .orElseThrow(() -> new IllegalStateException("회원이 null일 수는 없습니다")); SocialAccount socialAccount = SocialAccount.builder() @@ -69,11 +69,6 @@ public void updateToken(Long socialAccountId, String accessToken, String refresh socialAccount.updateToken(accessToken, refreshToken, tokenExpiresAt); } - public Long findMemberId(String provider, String providerUserId) { - return socialAccountRepository.findMemberIdByProviderAndProviderUserId(provider, providerUserId) - .orElseThrow(() -> new IllegalStateException("회원 정보가 없습니다. 먼저 회원 정보를 생성해주세요")); - } - public List findAllProviders(Long memberId) { List accounts = socialAccountRepository.findAllByMemberId(memberId); return accounts.stream() diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/AuthResultDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/AuthResultDto.java new file mode 100644 index 0000000..0f96625 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/dto/AuthResultDto.java @@ -0,0 +1,12 @@ +package com.stcom.smartmealtable.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AuthResultDto { + private Long memberId; + private Long profileId; + private boolean newUser; +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/MemberDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/MemberDto.java new file mode 100644 index 0000000..8603826 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/dto/MemberDto.java @@ -0,0 +1,24 @@ +package com.stcom.smartmealtable.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberDto { + private Long memberId; + private Long profileId; + private String email; + + public static MemberDto createFrom(Long memberId, Long profileId, String email) { + return MemberDto.builder() + .memberId(memberId) + .profileId(profileId) + .email(email) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java index bdf44c3..25d2099 100644 --- a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java +++ b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java @@ -1,22 +1,31 @@ package com.stcom.smartmealtable.web; -import com.stcom.smartmealtable.web.argumentresolver.JwtAuthorizationArgumentResolver; +import com.stcom.smartmealtable.web.argumentresolver.UserContextArgumentResolver; +import com.stcom.smartmealtable.web.interceptor.JwtAuthenticationInterceptor; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { - private final JwtAuthorizationArgumentResolver jwtAuthorizationArgumentResolver; + private final UserContextArgumentResolver userContextArgumentResolver; + private final JwtAuthenticationInterceptor jwtAuthenticationInterceptor; @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(jwtAuthorizationArgumentResolver); + resolvers.add(userContextArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtAuthenticationInterceptor) + .addPathPatterns("/api/**"); } @Override diff --git a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java deleted file mode 100644 index 13b9d6e..0000000 --- a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/JwtAuthorizationArgumentResolver.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.stcom.smartmealtable.web.argumentresolver; - -import com.stcom.smartmealtable.security.JwtAuthorization; -import com.stcom.smartmealtable.security.JwtTokenService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -@Component -@RequiredArgsConstructor -public class JwtAuthorizationArgumentResolver implements HandlerMethodArgumentResolver { - - private final JwtTokenService jwtTokenService; - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(JwtAuthorization.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); - - // 헤더 값 체크 - if (httpServletRequest != null) { - String token = httpServletRequest.getHeader("Authorization"); - - if (token != null && !token.trim().equals("")) { - // 토큰 있을 경우 검증 - jwtTokenService.validateToken(token); - return jwtTokenService.getClaim(token); - } - } - - // 토큰 값이 없으면 에러 - throw new RuntimeException("권한 없음."); - } -} diff --git a/src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContext.java similarity index 80% rename from src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java rename to src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContext.java index 45a4508..0fa44db 100644 --- a/src/main/java/com/stcom/smartmealtable/security/JwtAuthorization.java +++ b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContext.java @@ -1,4 +1,4 @@ -package com.stcom.smartmealtable.security; +package com.stcom.smartmealtable.web.argumentresolver; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -9,7 +9,7 @@ @Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface JwtAuthorization { +public @interface UserContext { boolean required() default true; } diff --git a/src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolver.java b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolver.java new file mode 100644 index 0000000..47d14e9 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolver.java @@ -0,0 +1,66 @@ +package com.stcom.smartmealtable.web.argumentresolver; + +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class UserContextArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtTokenService jwtTokenService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserContext.class) && + MemberDto.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + UserContext annotation = parameter.getParameterAnnotation(UserContext.class); + + if (httpServletRequest == null) { + throw new RuntimeException("예상치 못한 오류 발생."); + } + + String token = httpServletRequest.getHeader("Authorization"); + if (token == null || token.trim().isEmpty()) { + throw new RuntimeException("권한 없음."); + } + + return extractUserContext(token); + } + + private MemberDto extractUserContext(String token) { + if (token.startsWith("Bearer ")) { + token = token.substring(7); + } + + var claims = jwtTokenService.extractClaims(token); + + String memberIdStr = claims.get("memberId", String.class); + Long memberId = Long.parseLong(memberIdStr); + + // profileId 추출 (있는 경우) + Long profileId = null; + if (claims.containsKey("profileId")) { + String profileIdStr = claims.get("profileId", String.class); + profileId = Long.parseLong(profileIdStr); + } + + // email 추출 (있는 경우) + String email = claims.containsKey("email") ? claims.get("email", String.class) : null; + + return new MemberDto(memberId, profileId, email); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java similarity index 67% rename from src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java rename to src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java index 8493470..b0af479 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/LoginController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java @@ -1,12 +1,11 @@ -package com.stcom.smartmealtable.web.auth; +package com.stcom.smartmealtable.web.controller; -import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.security.JwtAuthorization; import com.stcom.smartmealtable.security.JwtBlacklistService; import com.stcom.smartmealtable.security.JwtTokenService; import com.stcom.smartmealtable.service.LoginService; +import com.stcom.smartmealtable.service.dto.AuthResultDto; import com.stcom.smartmealtable.web.dto.ApiResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.Email; @@ -14,6 +13,7 @@ import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -30,26 +30,20 @@ public class LoginController { private final JwtBlacklistService jwtBlacklistService; @PostMapping("/login") - public ApiResponse login(@JwtAuthorization Member member, @RequestBody LoginRequest request) + public ApiResponse login(@Validated @RequestBody LoginRequest request) throws PasswordFailedExceededException { - if (member == null) { - member = loginService.login(request.getEmail(), request.getPassword()); + AuthResultDto authResultDto = loginService.loginWithEmail(request.getEmail(), request.getPassword()); + JwtTokenResponseDto jwtTokenResponseDto = + jwtTokenService.createTokenDto(authResultDto.getMemberId(), authResultDto.getProfileId()); + if (authResultDto.isNewUser()) { + jwtTokenResponseDto.setNewUser(true); } - - JwtTokenResponseDto tokenResponseDto = jwtTokenService.createTokenDto(member.getId()); - if (member.getMemberProfile() == null) { - tokenResponseDto.setNewUser(true); - } - log.info("response = {}", tokenResponseDto); - return ApiResponse.createSuccess(tokenResponseDto); + return ApiResponse.createSuccess(jwtTokenResponseDto); } @PostMapping("/logout") public ApiResponse logout(HttpServletRequest request) { String jwt = request.getHeader("Authorization"); - if (jwt.isBlank()) { - return ApiResponse.createError("인증 토큰이 없습니다"); - } jwtBlacklistService.addToBlacklist(jwt); return ApiResponse.createSuccessWithNoContent(); } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java new file mode 100644 index 0000000..af3655b --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java @@ -0,0 +1,109 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.infrastructure.AddressApiService; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.BudgetService; +import com.stcom.smartmealtable.service.FoodPreferenceService; +import com.stcom.smartmealtable.service.MemberService; +import com.stcom.smartmealtable.service.SocialAccountService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Slf4j +@RequiredArgsConstructor +@RequestMapping("/api/v1/members") +public class MemberController { + + private final MemberService memberService; + private final JwtTokenService jwtTokenService; + private final AddressApiService addressApiService; + private final BudgetService budgetService; + private final SocialAccountService socialAccountService; + private final FoodPreferenceService foodPreferenceService; + + @GetMapping("/email/check") + public ResponseEntity> checkEmail(@Email @RequestParam String email) { + memberService.validateDuplicatedEmail(email); + return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping() + public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, + BindingResult bindingResult) throws PasswordPolicyException { + memberService.validateDuplicatedEmail(request.getEmail()); + memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); + + Member member = Member.builder() + .fullName(request.getFullName()) + .email(request.getEmail()) + .rawPassword(request.getPassword()) + .build(); + + memberService.saveMember(member); + JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId(), null); + tokenDto.setNewUser(true); + return ApiResponse.createSuccess(tokenDto); + } + + @PatchMapping("/me") + public ApiResponse editMember(@UserContext MemberDto memberDto, @Valid @RequestBody EditMemberRequest request, + BindingResult bindingResult) + throws PasswordPolicyException, PasswordFailedExceededException { + memberService.checkPasswordDoubly(request.getNewPassword(), request.getConfirmPassword()); + memberService.changePassword(memberDto.getMemberId(), request.getOriginPassword(), request.getNewPassword()); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/me") + public ApiResponse deleteMember(@UserContext MemberDto memberDto) { + memberService.deleteByMemberId(memberDto.getMemberId()); + return ApiResponse.createSuccessWithNoContent(); + } + + + @Data + @AllArgsConstructor + static class CreateMemberRequest { + + @Email + private String email; + private String password; + private String confirmPassword; + private String fullName; + } + + @Data + @AllArgsConstructor + static class EditMemberRequest { + + private String originPassword; + private String newPassword; + private String confirmPassword; + } + +} diff --git a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java similarity index 55% rename from src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java rename to src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java index 4d7fa3e..ee40581 100644 --- a/src/main/java/com/stcom/smartmealtable/web/auth/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java @@ -1,12 +1,14 @@ -package com.stcom.smartmealtable.web.auth; +package com.stcom.smartmealtable.web.controller; import com.stcom.smartmealtable.infrastructure.SocialAuthService; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.infrastructure.dto.TokenDto; import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.SocialAccountService; +import com.stcom.smartmealtable.service.LoginService; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; import com.stcom.smartmealtable.web.dto.ApiResponse; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -24,39 +26,27 @@ @RequiredArgsConstructor public class OAuth2Controller { - private final SocialAuthService socialManager; - private final SocialAccountService socialAccountService; private final JwtTokenService jwtTokenService; private final SocialAuthService socialAuthService; - private final MemberService memberService; + private final LoginService loginService; @PostMapping("/oauth2/code") public ApiResponse getTokenFromSocial(@RequestBody JwtTokenRequest request) { - log.info("request = {}", request); - TokenDto token = socialAuthService.getTokenResponse(request.getProvider().toLowerCase(), - request.getAuthorizationCode()); - log.info("response 성공 = {}", token); - boolean isNewMember = memberService.isNewMember(token.getEmail()); - boolean isSocialNewUser = socialAccountService.isNewUser(token.getProvider(), - token.getProviderUserId()); - if (isNewMember && isSocialNewUser) { // 소셜, 회원 모두 신규 - socialAccountService.createNewMemberAndLinkSocialAccount(token); - } else if (isSocialNewUser) { // 소셜만 신규, 회원은 존재 - socialAccountService.linkSocialAccount(token); + TokenDto token = socialAuthService.getTokenResponse( + request.getProvider().toLowerCase(), request.getAuthorizationCode()); + AuthResultDto authResultDto = loginService.socialLogin(token); + JwtTokenResponseDto jwtDto = + jwtTokenService.createTokenDto(authResultDto.getMemberId(), authResultDto.getProfileId()); + if (authResultDto.isNewUser()) { + jwtDto.setNewUser(true); } - - JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto( - socialAccountService.findMemberId(token.getProvider(), token.getProviderUserId())); - tokenDto.setNewUser(isSocialNewUser); - log.info("response = {}", tokenDto); - return ApiResponse.createSuccess(tokenDto); + return ApiResponse.createSuccess(jwtDto); } @PostMapping("/api/v1/auth/token/refresh") - public ApiResponse refreshAccessToken(@RequestBody JwtRefreshTokenRequest request) { - String memberId = jwtTokenService.extractMemberIdFromRefreshToken(request.getRefreshToken()); - String accessToken = jwtTokenService.createAccessToken(memberId); - + public ApiResponse refreshAccessToken(@UserContext MemberDto memberDto, + @RequestBody JwtRefreshTokenRequest request) { + String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId()); return ApiResponse.createSuccess( new JwtRefreshedAccessTokenDto(accessToken, 3600, "Bearar") ); diff --git a/src/main/java/com/stcom/smartmealtable/web/interceptor/JwtAuthenticationInterceptor.java b/src/main/java/com/stcom/smartmealtable/web/interceptor/JwtAuthenticationInterceptor.java new file mode 100644 index 0000000..b6e3c33 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/interceptor/JwtAuthenticationInterceptor.java @@ -0,0 +1,51 @@ +package com.stcom.smartmealtable.web.interceptor; + +import com.stcom.smartmealtable.security.JwtTokenService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationInterceptor implements HandlerInterceptor { + + private final JwtTokenService jwtTokenService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 인증이 필요 없는 경로 제외 (필요에 따라 설정) + String path = request.getRequestURI(); + if (path.startsWith("/api/v1/auth/login") || + path.startsWith("/api/v1/auth/oauth2") || + path.startsWith("/api/v1/auth/token/refresh")) { + return true; + } + + // OPTIONS 요청은 CORS preflight 요청으로 인증 필요 없음 + if (request.getMethod().equals("OPTIONS")) { + return true; + } + + // 토큰 존재 확인 + String token = request.getHeader("Authorization"); + if (token == null || token.trim().isEmpty()) { + return true; // 토큰이 없는 요청은 통과시키고, ArgumentResolver에서 처리 + } + + // 토큰 검증 + try { + jwtTokenService.validateToken(token); + return true; + } catch (Exception e) { + log.error("토큰 검증 실패: {}", e.getMessage()); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java deleted file mode 100644 index 3091cca..0000000 --- a/src/main/java/com/stcom/smartmealtable/web/member/MemberController.java +++ /dev/null @@ -1,205 +0,0 @@ -package com.stcom.smartmealtable.web.member; - -import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.domain.Address.AddressType; -import com.stcom.smartmealtable.domain.Budget.DailyBudget; -import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; -import com.stcom.smartmealtable.domain.food.FoodCategory; -import com.stcom.smartmealtable.domain.member.Member; -import com.stcom.smartmealtable.domain.member.MemberGroup; -import com.stcom.smartmealtable.domain.member.MemberProfile; -import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.infrastructure.AddressApiService; -import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; -import com.stcom.smartmealtable.security.JwtAuthorization; -import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.BudgetService; -import com.stcom.smartmealtable.service.FoodPreferenceService; -import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.SocialAccountService; -import com.stcom.smartmealtable.web.dto.ApiResponse; -import jakarta.validation.Valid; -import jakarta.validation.constraints.Email; -import java.util.ArrayList; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.GetMapping; -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.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@Slf4j -@RequiredArgsConstructor -@RequestMapping("/api/v1/members") -public class MemberController { - - private final MemberService memberService; - private final JwtTokenService jwtTokenService; - private final AddressApiService addressApiService; - private final BudgetService budgetService; - private final SocialAccountService socialAccountService; - private final FoodPreferenceService foodPreferenceService; - - @GetMapping("/email/check") - public ResponseEntity> checkEmail(@Email @RequestParam String email) { - memberService.validateDuplicatedEmail(email); - return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); - } - - @ResponseStatus(HttpStatus.CREATED) - @PostMapping() - public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, - BindingResult bindingResult) throws PasswordPolicyException { - memberService.validateDuplicatedEmail(request.getEmail()); - memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); - - Member member = Member.builder() - .fullName(request.getFullName()) - .email(request.getEmail()) - .rawPassword(request.getPassword()) - .build(); - - memberService.saveMember(member); - JwtTokenResponseDto tokenDto = jwtTokenService.createTokenDto(member.getId()); - tokenDto.setNewUser(true); - return ApiResponse.createSuccess(tokenDto); - } - - @ResponseStatus(HttpStatus.CREATED) - @PostMapping("/profile") - public ApiResponse createMemberProfile(@JwtAuthorization Member member, - @RequestBody CreateMemberProfileRequest request) { - // request 주소로 부터 위도, 경도 얻기(카카오 local api) - Address address = getAddressFromApiService(request.getHomeAddress()); - MemberProfile memberProfile = request.toEntity(); - memberProfile.addAddress(address); - memberService.linkMember(member.getId(), memberProfile); - - return ApiResponse.createSuccessWithNoContent(); - } - - @GetMapping("/profile/me") - public ApiResponse memberProfile(@JwtAuthorization Member member) { - MemberProfile memberProfile = memberService.findMemberProfileByMemberId(member.getId()); - DailyBudget dailyBudget = budgetService.findRecentDailyBudgetByMemberId(member.getId()); - MonthlyBudget monthlyBudget = budgetService.findRecentMonthlyBudgetByMemberId(member.getId()); - List providers = socialAccountService.findAllProviders(member.getId()); - List foodCategories = foodPreferenceService.findPreferredFoodCategories(member); - MemberProfileResponse memberProfileResponse = - buildMemberProfileResponse(member, memberProfile, foodCategories, dailyBudget, monthlyBudget); - return ApiResponse.createSuccess(memberProfileResponse); - } - - private MemberProfileResponse buildMemberProfileResponse(Member member, MemberProfile memberProfile, - List foodCategories, DailyBudget dailyBudget, - MonthlyBudget monthlyBudget) { - return MemberProfileResponse.builder() - .memberGroup(memberProfile.getMemberGroup()) - .groupName(memberProfile.getGroupName()) - .email(member.getEmail()) - .nickName(memberProfile.getNickName()) - .preferredFoodCategory(foodCategories) - .dailyLimitAmount(dailyBudget.getLimit().longValue()) - .dailyAvailableAmount(dailyBudget.getAvailableAmount().longValue()) - .monthlyLimitAmount(monthlyBudget.getLimit().longValue()) - .monthlyAvailableAmount(monthlyBudget.getAvailableAmount().longValue()) - .addressList(memberProfile.getAddressHistory().stream() - .map(a -> a.getRoadAddress() + a.getDetailAddress()) - .toList()) - .build(); - } - - private Address getAddressFromApiService(AddressRequest request) { - com.stcom.smartmealtable.infrastructure.dto.AddressRequest dto = new com.stcom.smartmealtable.infrastructure.dto.AddressRequest( - request.getRoadAddress(), AddressType.HOME, "집", - request.getDetailAddress()); - return addressApiService.createAddressFromRequest(dto); - } - - @Data - @AllArgsConstructor - static class CreateMemberRequest { - - @Email - private String email; - private String password; - private String confirmPassword; - private String fullName; - } - - @Data - @NoArgsConstructor - static class CreateMemberProfileRequest { - private MemberGroup groupType; - private String groupName; - private AddressRequest homeAddress; - private List foodPreference; - private List hateFoods; - private Long dailyLimitAmount; - private Long monthlyLimitAmount; - - public MemberProfile toEntity() { - return MemberProfile.builder() - .memberGroup(groupType) - .groupName(groupName) - .nickName("test") - .addressHistory(new ArrayList<>()) - .build(); - } - } - - @Data - @AllArgsConstructor - static class AddressRequest { - private int zonecode; - private String lotNumberAddress; - private String roadAddress; - private String detailAddress; - } - - @Data - @NoArgsConstructor - static class MemberProfileResponse { - private String nickName; - private List preferredFoodCategory; - private String email; - private List addressList; - private MemberGroup memberGroup; - private String groupName; - private Long dailyAvailableAmount; - private Long dailyLimitAmount; - private Long monthlyAvailableAmount; - private Long monthlyLimitAmount; - - @Builder - public MemberProfileResponse(String nickName, List preferredFoodCategory, String email, - List addressList, MemberGroup memberGroup, String groupName, - Long dailyAvailableAmount, - Long dailyLimitAmount, Long monthlyAvailableAmount, Long monthlyLimitAmount) { - this.nickName = nickName; - this.preferredFoodCategory = preferredFoodCategory; - this.email = email; - this.addressList = addressList; - this.memberGroup = memberGroup; - this.groupName = groupName; - this.dailyAvailableAmount = dailyAvailableAmount; - this.dailyLimitAmount = dailyLimitAmount; - this.monthlyAvailableAmount = monthlyAvailableAmount; - this.monthlyLimitAmount = monthlyLimitAmount; - } - } - - -} From 77e94ae9df84963783bd4b88120122163d4b17c4 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 21:43:03 +0900 Subject: [PATCH 080/120] =?UTF-8?q?feat:=20Group=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemberGroup 열거형 -> Group 엔티티로 --- .../domain/group/CompanyGroup.java | 16 +++++++++ .../smartmealtable/domain/group/Group.java | 35 +++++++++++++++++++ .../domain/group/GroupType.java | 5 +++ .../domain/group/IndustryType.java | 5 +++ .../domain/group/SchoolGroup.java | 16 +++++++++ .../domain/group/SchoolType.java | 5 +++ .../repository/GroupRepository.java | 8 +++++ .../smartmealtable/service/GroupService.java | 18 ++++++++++ .../smartmealtable/service/dto/GroupDto.java | 14 ++++++++ .../web/controller/GroupController.java | 14 ++++++++ 10 files changed, 136 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/Group.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/GroupService.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java b/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java new file mode 100644 index 0000000..8803f07 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java @@ -0,0 +1,16 @@ +package com.stcom.smartmealtable.domain.group; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class CompanyGroup extends Group { + + @Enumerated(EnumType.STRING) + private IndustryType industryType; +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java new file mode 100644 index 0000000..fb3401d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java @@ -0,0 +1,35 @@ +package com.stcom.smartmealtable.domain.group; + +import com.stcom.smartmealtable.domain.Address.Address; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "groups") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn +@Getter +@NoArgsConstructor +public abstract class Group { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Embedded + private Address address; + + private String name; + + private GroupType groupType; + +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java b/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java new file mode 100644 index 0000000..883e6b5 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.group; + +public enum GroupType { + UNIVERSITY, COMPANY, OTHER, NONE +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java new file mode 100644 index 0000000..f641ca0 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.group; + +public enum IndustryType { + +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java new file mode 100644 index 0000000..1c3a2aa --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java @@ -0,0 +1,16 @@ +package com.stcom.smartmealtable.domain.group; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class SchoolGroup extends Group { + + @Enumerated(EnumType.STRING) + private SchoolType schoolType; +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java new file mode 100644 index 0000000..51023b1 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.group; + +public enum SchoolType { + UNIVERSITY_FOUR_YEAR, UNIVERSITY_TWO_YEAR, HIGH_SCHOOL, MIDDLE_SCHOOL +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java b/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java new file mode 100644 index 0000000..443788f --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java @@ -0,0 +1,8 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.group.Group; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GroupRepository extends JpaRepository { + +} diff --git a/src/main/java/com/stcom/smartmealtable/service/GroupService.java b/src/main/java/com/stcom/smartmealtable/service/GroupService.java new file mode 100644 index 0000000..fd10e42 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/GroupService.java @@ -0,0 +1,18 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.repository.GroupRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GroupService { + + private final GroupRepository groupRepository; + + public Group findGroupByGroupId(Long groupId) { + return groupRepository.findById(groupId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java new file mode 100644 index 0000000..0653ca4 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java @@ -0,0 +1,14 @@ +package com.stcom.smartmealtable.service.dto; + +import com.stcom.smartmealtable.domain.group.GroupType; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class GroupDto { + + private Long id; + private GroupType groupType; + private String address; +} diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java new file mode 100644 index 0000000..225b9ae --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java @@ -0,0 +1,14 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.service.GroupService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/groups") +public class GroupController { + + private final GroupService groupService; +} From 3d99620fb79d6de1a030353b571283450316a6f6 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 21:43:24 +0900 Subject: [PATCH 081/120] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C/=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MemberProfileService.java | 45 ++++++++++++ .../controller/MemberProfileController.java | 68 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java new file mode 100644 index 0000000..5568a30 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -0,0 +1,45 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.GroupType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.GroupRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberProfileService { + + private final MemberProfileRepository memberProfileRepository; + private final GroupRepository groupRepository; + private final MemberRepository memberRepository; + + public MemberProfile getProfileFetch(Long profileId) { + return memberProfileRepository.findMemberProfileEntityGraphById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + } + + @Transactional + public void createProfile(String nickName, Long memberId, GroupType groupType, Long groupId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); + + // TODO: 실제 프록시 객체 초기화 시점에 인스턴스의 서브타입이 결정된다는데, 테스트해보기 + Group group = (groupId != null) + ? groupRepository.getReferenceById(groupId) + : null; + + MemberProfile profile = MemberProfile.builder() + .nickName(nickName) + .member(member) + .group(group) + .build(); + + memberProfileRepository.save(profile); + } +} diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java new file mode 100644 index 0000000..6b27ab8 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -0,0 +1,68 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.GroupType; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.service.MemberProfileService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/members/profile") +public class MemberProfileController { + + private final MemberProfileService memberProfileService; + + @GetMapping("/me") + public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { + MemberProfile profile = memberProfileService.getProfileFetch(memberDto.getProfileId()); + return ApiResponse.createSuccess(new MemberProfilePageResponse(profile, memberDto)); + } + + @PostMapping() + public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, + @Validated @RequestBody MemberProfileCreateRequest request) { + memberProfileService.createProfile(request.getNickName(), memberDto.getMemberId(), request.getGroupType(), + request.getGroupId()); + return ApiResponse.createSuccessWithNoContent(); + } + + @AllArgsConstructor + @Data + static class MemberProfilePageResponse { + private String nickName; + private String email; + private GroupType groupType; + private String groupName; + private String primaryAddress; + + public MemberProfilePageResponse(MemberProfile profile, MemberDto memberDto) { + this.nickName = profile.getNickName(); + this.email = memberDto.getEmail(); + Address address = profile.findPrimaryAddress().getAddress(); + this.primaryAddress = address.getRoadAddress() + address.getDetailAddress(); + this.groupType = profile.getGroup().getGroupType(); + this.groupName = profile.getGroup().getName(); + } + } + + @AllArgsConstructor + @Data + static class MemberProfileCreateRequest { + private String nickName; + private Long groupId; + private GroupType groupType; + } + +} From 472364b62bae4a43eb7aad7e48eebd241724e359 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:00:54 +0900 Subject: [PATCH 082/120] =?UTF-8?q?refactor:=20Group=EA=B3=BC=20Type=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 소속 그룹과, 회원의 유형을 분리 --- .../smartmealtable/domain/group/Group.java | 4 +--- .../domain/group/GroupType.java | 5 ----- .../domain/member/MemberProfile.java | 13 +++++++++++- .../domain/member/MemberType.java | 8 ++++++++ .../service/MemberProfileService.java | 17 ++++++++++++++-- .../controller/MemberProfileController.java | 20 ++++++++++++------- 6 files changed, 49 insertions(+), 18 deletions(-) delete mode 100644 src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java index fb3401d..90491cb 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java @@ -29,7 +29,5 @@ public abstract class Group { private Address address; private String name; - - private GroupType groupType; - + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java b/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java deleted file mode 100644 index 883e6b5..0000000 --- a/src/main/java/com/stcom/smartmealtable/domain/group/GroupType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.stcom.smartmealtable.domain.group; - -public enum GroupType { - UNIVERSITY, COMPANY, OTHER, NONE -} diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index ec6c4d9..007c62b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -6,6 +6,7 @@ import com.stcom.smartmealtable.domain.group.Group; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -41,15 +42,21 @@ public class MemberProfile extends BaseTimeEntity { @JoinColumn(name = "member_id") private List addressHistory = new ArrayList<>(); + @Embedded + private MemberType type; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id") private Group group; + @Builder - public MemberProfile(Member member, String nickName, List addressHistory, Group group) { + public MemberProfile(Member member, String nickName, List addressHistory, MemberType type, + Group group) { linkMember(member); this.nickName = nickName; this.addressHistory = addressHistory; + this.type = type; this.group = group; } @@ -84,4 +91,8 @@ public void setPrimaryAddress(AddressEntity target) { addressHistory.forEach(AddressEntity::unmarkPrimary); target.markPrimary(); } + + public void changeGroup(Group newGroup) { + this.group = newGroup; + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java new file mode 100644 index 0000000..ce8cebf --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java @@ -0,0 +1,8 @@ +package com.stcom.smartmealtable.domain.member; + +import jakarta.persistence.Embeddable; + +@Embeddable +public enum MemberType { + STUDENT, WORKER, OTHER +} diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index 5568a30..c8bade1 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -1,9 +1,9 @@ package com.stcom.smartmealtable.service; import com.stcom.smartmealtable.domain.group.Group; -import com.stcom.smartmealtable.domain.group.GroupType; import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; import com.stcom.smartmealtable.repository.GroupRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; @@ -25,7 +25,7 @@ public MemberProfile getProfileFetch(Long profileId) { } @Transactional - public void createProfile(String nickName, Long memberId, GroupType groupType, Long groupId) { + public void createProfile(String nickName, Long memberId, MemberType type, Long groupId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); @@ -37,9 +37,22 @@ public void createProfile(String nickName, Long memberId, GroupType groupType, L MemberProfile profile = MemberProfile.builder() .nickName(nickName) .member(member) + .type(type) .group(group) .build(); memberProfileRepository.save(profile); } + + @Transactional + public void changeProfile(Long profileId, String nickName, MemberProfile type, Long groupId) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + + profile.changeNickName(nickName); + Group newGroup = (groupId != null) + ? groupRepository.getReferenceById(groupId) + : null; + profile.changeGroup(newGroup); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 6b27ab8..bc9cd93 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -1,8 +1,8 @@ package com.stcom.smartmealtable.web.controller; import com.stcom.smartmealtable.domain.Address.Address; -import com.stcom.smartmealtable.domain.group.GroupType; import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; import com.stcom.smartmealtable.service.MemberProfileService; import com.stcom.smartmealtable.service.dto.MemberDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; @@ -32,18 +32,24 @@ public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) @PostMapping() public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, - @Validated @RequestBody MemberProfileCreateRequest request) { - memberProfileService.createProfile(request.getNickName(), memberDto.getMemberId(), request.getGroupType(), + @Validated @RequestBody MemberProfileRequest request) { + memberProfileService.createProfile(request.getNickName(), memberDto.getMemberId(), request.getMemberType(), request.getGroupId()); return ApiResponse.createSuccessWithNoContent(); } +// @PostMapping() +// public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, +// @Validated @RequestBody MemberProfileRequest request) { +// +// } + @AllArgsConstructor @Data static class MemberProfilePageResponse { private String nickName; private String email; - private GroupType groupType; + private MemberType memberType; private String groupName; private String primaryAddress; @@ -52,17 +58,17 @@ public MemberProfilePageResponse(MemberProfile profile, MemberDto memberDto) { this.email = memberDto.getEmail(); Address address = profile.findPrimaryAddress().getAddress(); this.primaryAddress = address.getRoadAddress() + address.getDetailAddress(); - this.groupType = profile.getGroup().getGroupType(); + this.memberType = profile.getType(); this.groupName = profile.getGroup().getName(); } } @AllArgsConstructor @Data - static class MemberProfileCreateRequest { + static class MemberProfileRequest { private String nickName; private Long groupId; - private GroupType groupType; + private MemberType memberType; } } From 0b3c9ff88d170efc59a3144d0f522cf25b8f9c0a Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:04:18 +0900 Subject: [PATCH 083/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EB=B3=80=EA=B2=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/MemberProfile.java | 4 ++++ .../service/MemberProfileService.java | 3 ++- .../web/controller/MemberProfileController.java | 13 ++++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 007c62b..eb2a077 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -87,6 +87,10 @@ public void changeNickName(String newNickName) { this.nickName = newNickName; } + public void changeMemberType(MemberType memberType) { + this.type = memberType; + } + public void setPrimaryAddress(AddressEntity target) { addressHistory.forEach(AddressEntity::unmarkPrimary); target.markPrimary(); diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index c8bade1..a624e56 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -45,11 +45,12 @@ public void createProfile(String nickName, Long memberId, MemberType type, Long } @Transactional - public void changeProfile(Long profileId, String nickName, MemberProfile type, Long groupId) { + public void changeProfile(Long profileId, String nickName, MemberType type, Long groupId) { MemberProfile profile = memberProfileRepository.findById(profileId) .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); profile.changeNickName(nickName); + profile.changeMemberType(type); Group newGroup = (groupId != null) ? groupRepository.getReferenceById(groupId) : null; diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index bc9cd93..53f1ee4 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -12,6 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,11 +39,13 @@ public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, return ApiResponse.createSuccessWithNoContent(); } -// @PostMapping() -// public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, -// @Validated @RequestBody MemberProfileRequest request) { -// -// } + @PatchMapping + public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, + @Validated @RequestBody MemberProfileRequest request) { + memberProfileService.changeProfile(memberDto.getProfileId(), request.getNickName(), request.getMemberType(), + request.getGroupId()); + return ApiResponse.createSuccessWithNoContent(); + } @AllArgsConstructor @Data From c92b227c90e15a3f47a66732fddd05ff212a3293 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:18:22 +0900 Subject: [PATCH 084/120] =?UTF-8?q?feat:=20=EB=8C=80=ED=91=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/AddressEntityRepository.java | 8 ++++++++ .../smartmealtable/repository/AddressRepository.java | 8 -------- .../stcom/smartmealtable/service/AddressService.java | 4 ++-- .../smartmealtable/service/MemberProfileService.java | 12 ++++++++++++ .../stcom/smartmealtable/service/MemberService.java | 4 ++-- .../web/controller/MemberProfileController.java | 6 ++++++ 6 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/repository/AddressEntityRepository.java delete mode 100644 src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java diff --git a/src/main/java/com/stcom/smartmealtable/repository/AddressEntityRepository.java b/src/main/java/com/stcom/smartmealtable/repository/AddressEntityRepository.java new file mode 100644 index 0000000..194dd70 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/AddressEntityRepository.java @@ -0,0 +1,8 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.Address.AddressEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AddressEntityRepository extends JpaRepository { + +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java b/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java deleted file mode 100644 index a604c1d..0000000 --- a/src/main/java/com/stcom/smartmealtable/repository/AddressRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.stcom.smartmealtable.repository; - -import com.stcom.smartmealtable.domain.Address.Address; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface AddressRepository extends JpaRepository { - -} diff --git a/src/main/java/com/stcom/smartmealtable/service/AddressService.java b/src/main/java/com/stcom/smartmealtable/service/AddressService.java index 87ae77a..10475ab 100644 --- a/src/main/java/com/stcom/smartmealtable/service/AddressService.java +++ b/src/main/java/com/stcom/smartmealtable/service/AddressService.java @@ -1,6 +1,6 @@ package com.stcom.smartmealtable.service; -import com.stcom.smartmealtable.repository.AddressRepository; +import com.stcom.smartmealtable.repository.AddressEntityRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -8,5 +8,5 @@ @RequiredArgsConstructor public class AddressService { - private final AddressRepository addressRepository; + private final AddressEntityRepository addressEntityRepository; } diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index a624e56..a5375fe 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -1,9 +1,11 @@ package com.stcom.smartmealtable.service; +import com.stcom.smartmealtable.domain.Address.AddressEntity; import com.stcom.smartmealtable.domain.group.Group; import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.AddressEntityRepository; import com.stcom.smartmealtable.repository.GroupRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; @@ -18,6 +20,7 @@ public class MemberProfileService { private final MemberProfileRepository memberProfileRepository; private final GroupRepository groupRepository; private final MemberRepository memberRepository; + private final AddressEntityRepository addressEntityRepository; public MemberProfile getProfileFetch(Long profileId) { return memberProfileRepository.findMemberProfileEntityGraphById(profileId) @@ -56,4 +59,13 @@ public void changeProfile(Long profileId, String nickName, MemberType type, Long : null; profile.changeGroup(newGroup); } + + @Transactional + public void changeAddressToPrimary(Long profileId, Long addressId) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + AddressEntity targetAddressEntity = addressEntityRepository.findById(addressId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); + profile.setPrimaryAddress(targetAddressEntity); + } } diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index 8212e4d..79c6e60 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -3,7 +3,7 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.repository.AddressRepository; +import com.stcom.smartmealtable.repository.AddressEntityRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; import com.stcom.smartmealtable.repository.SocialAccountRepository; @@ -18,7 +18,7 @@ public class MemberService { private final MemberRepository memberRepository; private final MemberProfileRepository memberProfileRepository; private final SocialAccountRepository socialAccountRepository; - private final AddressRepository addressRepository; + private final AddressEntityRepository addressEntityRepository; public void validateDuplicatedEmail(String email) { memberRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("이미 존재하는 이메일 입니다")); diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 53f1ee4..09e53f0 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -13,6 +13,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -47,6 +48,11 @@ public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, return ApiResponse.createSuccessWithNoContent(); } + @PostMapping("/addresses/{id}/primary") + public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { + memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); + } + @AllArgsConstructor @Data static class MemberProfilePageResponse { From c569f9ff48a114c6d7e925628e115ceacc7afb4e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:20:06 +0900 Subject: [PATCH 085/120] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EA=B3=84=EC=B8=B5=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조회만 하는 경우 readOnly=true로 변경 --- .../java/com/stcom/smartmealtable/service/AddressService.java | 2 ++ .../java/com/stcom/smartmealtable/service/BudgetService.java | 2 ++ .../com/stcom/smartmealtable/service/FoodPreferenceService.java | 2 ++ .../java/com/stcom/smartmealtable/service/GroupService.java | 2 ++ .../java/com/stcom/smartmealtable/service/LoginService.java | 2 ++ .../com/stcom/smartmealtable/service/MemberProfileService.java | 1 + .../java/com/stcom/smartmealtable/service/MemberService.java | 1 + 7 files changed, 12 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/service/AddressService.java b/src/main/java/com/stcom/smartmealtable/service/AddressService.java index 10475ab..e6be137 100644 --- a/src/main/java/com/stcom/smartmealtable/service/AddressService.java +++ b/src/main/java/com/stcom/smartmealtable/service/AddressService.java @@ -3,9 +3,11 @@ import com.stcom.smartmealtable.repository.AddressEntityRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class AddressService { private final AddressEntityRepository addressEntityRepository; diff --git a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java index 9ac53a4..08263ab 100644 --- a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java +++ b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java @@ -5,9 +5,11 @@ import com.stcom.smartmealtable.repository.BudgetRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class BudgetService { private final BudgetRepository budgetRepository; diff --git a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java index 9451cba..94d5e48 100644 --- a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java +++ b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java @@ -7,9 +7,11 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class FoodPreferenceService { private final FoodPreferenceRepository foodPreferenceRepository; diff --git a/src/main/java/com/stcom/smartmealtable/service/GroupService.java b/src/main/java/com/stcom/smartmealtable/service/GroupService.java index fd10e42..90f0783 100644 --- a/src/main/java/com/stcom/smartmealtable/service/GroupService.java +++ b/src/main/java/com/stcom/smartmealtable/service/GroupService.java @@ -4,9 +4,11 @@ import com.stcom.smartmealtable.repository.GroupRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class GroupService { private final GroupRepository groupRepository; diff --git a/src/main/java/com/stcom/smartmealtable/service/LoginService.java b/src/main/java/com/stcom/smartmealtable/service/LoginService.java index f1f1c1f..05ccbf2 100644 --- a/src/main/java/com/stcom/smartmealtable/service/LoginService.java +++ b/src/main/java/com/stcom/smartmealtable/service/LoginService.java @@ -14,11 +14,13 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class LoginService { private final MemberRepository memberRepository; private final SocialAccountRepository socialAccountRepository; + @Transactional public AuthResultDto loginWithEmail(String email, String password) throws PasswordFailedExceededException { Member findMember = memberRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index a5375fe..0307e64 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -15,6 +15,7 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MemberProfileService { private final MemberProfileRepository memberProfileRepository; diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index 79c6e60..c7a738f 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -13,6 +13,7 @@ @Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class MemberService { private final MemberRepository memberRepository; From 4cdf6aa56e48f77da8a46fe7796e1cf6b08f7bb4 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:21:07 +0900 Subject: [PATCH 086/120] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD=20=EC=9D=91=EB=8B=B5=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/web/controller/MemberProfileController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 09e53f0..454bf84 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -51,6 +51,7 @@ public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, @PostMapping("/addresses/{id}/primary") public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); + return ApiResponse.createSuccessWithNoContent(); } @AllArgsConstructor From b7720ed9ae6f5d39559cb1d3feb5fb107b46ced2 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:46:03 +0900 Subject: [PATCH 087/120] =?UTF-8?q?refactor:=20=EA=B0=92=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=ED=95=84=EB=93=9C=20=EC=9D=BC=EB=B6=80=EB=A5=BC=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EB=A1=9C=20=EC=9D=B4=EC=A0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/Address/Address.java | 13 +---------- .../domain/Address/AddressEntity.java | 23 +++++++++++++++++++ .../domain/member/MemberProfile.java | 17 ++++++++------ .../service/AddressService.java | 14 ----------- 4 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 src/main/java/com/stcom/smartmealtable/service/AddressService.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java index c1fa98f..293f7de 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/Address.java @@ -1,8 +1,6 @@ package com.stcom.smartmealtable.domain.Address; import jakarta.persistence.Embeddable; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,25 +16,18 @@ public class Address { private String detailAddress; - private String alias; - private Double latitude; private Double longitude; - @Enumerated(EnumType.STRING) - private AddressType type; - @Builder public Address(String lotNumberAddress, String roadAddress, String detailAddress, String alias, Double latitude, Double longitude, AddressType type) { this.lotNumberAddress = lotNumberAddress; this.roadAddress = roadAddress; this.detailAddress = detailAddress; - this.alias = alias; this.latitude = latitude; this.longitude = longitude; - this.type = type; } @@ -49,7 +40,5 @@ public void updateAddress(String lotNumberAddress, String roadAddress, String de this.longitude = longitude; } - public void changeAddressType(AddressType newType) { - this.type = newType; - } + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java index dabca3d..94877e2 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java @@ -3,10 +3,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -26,6 +29,18 @@ public class AddressEntity { private boolean primary = false; + @Enumerated(EnumType.STRING) + private AddressType type; + + private String alias; + + @Builder + public AddressEntity(Address address, AddressType type, String alias) { + this.address = address; + this.type = type; + this.alias = alias; + } + public AddressEntity(Address address) { this.address = address; } @@ -42,5 +57,13 @@ public boolean isPrimaryAddress() { return primary; } + public void changeAddressType(AddressType newType) { + this.type = newType; + } + + public void changeAlias(String newAlias) { + this.alias = newAlias; + } + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index eb2a077..d7cb93c 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -1,6 +1,5 @@ package com.stcom.smartmealtable.domain.member; -import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.domain.Address.AddressEntity; import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.group.Group; @@ -60,8 +59,11 @@ public MemberProfile(Member member, String nickName, List address this.group = group; } - public void addAddress(Address address) { - addressHistory.add(new AddressEntity(address)); + public void addAddress(AddressEntity addressEntity) { + addressHistory.add(addressEntity); + if (addressHistory.size() == 1) { + setPrimaryAddress(addressEntity); + } } public void removeAddress(AddressEntity addressEntity) { @@ -75,6 +77,11 @@ public AddressEntity findPrimaryAddress() { .orElseThrow(() -> new IllegalStateException("Primary Address가 없습니다")); } + public void setPrimaryAddress(AddressEntity target) { + addressHistory.forEach(AddressEntity::unmarkPrimary); + target.markPrimary(); + } + public void linkMember(Member member) { member.linkMemberProfile(this); this.member = member; @@ -91,10 +98,6 @@ public void changeMemberType(MemberType memberType) { this.type = memberType; } - public void setPrimaryAddress(AddressEntity target) { - addressHistory.forEach(AddressEntity::unmarkPrimary); - target.markPrimary(); - } public void changeGroup(Group newGroup) { this.group = newGroup; diff --git a/src/main/java/com/stcom/smartmealtable/service/AddressService.java b/src/main/java/com/stcom/smartmealtable/service/AddressService.java deleted file mode 100644 index e6be137..0000000 --- a/src/main/java/com/stcom/smartmealtable/service/AddressService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.stcom.smartmealtable.service; - -import com.stcom.smartmealtable.repository.AddressEntityRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class AddressService { - - private final AddressEntityRepository addressEntityRepository; -} From f3be26367a083192174cfa6869873cbfe1d5a75e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:47:09 +0900 Subject: [PATCH 088/120] =?UTF-8?q?refactor:=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EA=B2=B0=ED=95=A9=EB=8F=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemberProfileController-AddressApiService 느슨한 결합으로 개선 --- .../infrastructure/AddressApiService.java | 148 +---------------- .../KakaoAddressApiService.java | 151 ++++++++++++++++++ .../infrastructure/dto/AddressRequest.java | 3 - .../web/controller/MemberController.java | 4 +- 4 files changed, 155 insertions(+), 151 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiService.java diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java index 07b7375..cbbfa16 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/AddressApiService.java @@ -1,153 +1,9 @@ package com.stcom.smartmealtable.infrastructure; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; -@Service -public class AddressApiService { +public interface AddressApiService { - @Value("${kakao.oauth.client-id}") - private String clientId; - - private final RestClient client = RestClient.create(); - - public Address createAddressFromRequest(AddressRequest requestDto) { - AddressSearchResponse addressSearchResponse = client.get() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .host("dapi.kakao.com") - .path("/v2/local/search/address") - .queryParam("query", requestDto.getRoadAddress()) - .build()) - .header("Authorization", "KakaoAK " + clientId) - .retrieve() - .body(AddressSearchResponse.class); - - if (addressSearchResponse.getMeta().getTotalCount() >= 2) { - throw new IllegalArgumentException("주소가 모호합니다. 정확한 주소를 입력하세요"); - } - - return Address.builder() - .type(requestDto.getAddressType()) - .alias(requestDto.getAlias()) - .longitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLongitude())) - .latitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLatitude())) - .lotNumberAddress(addressSearchResponse.getDocuments().getFirst().getAddress().getAddressName()) - .roadAddress(addressSearchResponse.getDocuments().getFirst().getRoadAddress().getAddressName()) - .detailAddress(requestDto.getDetailAddress()) - .build(); - } - - @Data - @AllArgsConstructor - static class AddressSearchResponse { - private Meta meta; - private List documents; - } - - @Data - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - static class Meta { - private Integer totalCount; - private Integer pageableCount; - private Boolean isEnd; - } - - @Data - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - static class Document { - private String addressName; - - private String addressType; - - @JsonProperty("x") - private String longitude; - - @JsonProperty("y") - private String latitude; - - private LotAddress address; - - private RoadAddress roadAddress; - - } - - @Data - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - static class LotAddress { - private String addressName; - - @JsonProperty("region_1depth_name") - private String region1depthName; - - @JsonProperty("region_2depth_name") - private String region2depthName; - - @JsonProperty("region_3depth_name") - private String region3depthName; - - @JsonProperty("region_3depth_h_name") - private String region3depthHName; - - private String hCode; - - private String bCode; - - private String mountainYn; - - private String mainAddressNo; - - private String subAddressNo; - - private String x; - private String y; - } - - @Data - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - static class RoadAddress { - private String addressName; - - @JsonProperty("region_1depth_name") - private String region1depthName; - - @JsonProperty("region_2depth_name") - private String region2depthName; - - @JsonProperty("region_3depth_name") - private String region3depthName; - - private String roadName; - - @JsonProperty("underground_yn") - private String undergroundYn; - - @JsonProperty("main_building_no") - private String mainBuildingNo; - - @JsonProperty("sub_building_no") - private String subBuildingNo; - - @JsonProperty("building_name") - private String buildingName; - - @JsonProperty("zone_no") - private String zoneNo; - - private String x; - private String y; - } + Address createAddressFromRequest(AddressRequest requestDto); } diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiService.java b/src/main/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiService.java new file mode 100644 index 0000000..f24d7b0 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiService.java @@ -0,0 +1,151 @@ +package com.stcom.smartmealtable.infrastructure; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +public class KakaoAddressApiService implements AddressApiService { + + @Value("${kakao.oauth.client-id}") + private String clientId; + + private final RestClient client = RestClient.create(); + + public Address createAddressFromRequest(AddressRequest requestDto) { + AddressSearchResponse addressSearchResponse = client.get() + .uri(uriBuilder -> uriBuilder + .scheme("https") + .host("dapi.kakao.com") + .path("/v2/local/search/address") + .queryParam("query", requestDto.getRoadAddress()) + .build()) + .header("Authorization", "KakaoAK " + clientId) + .retrieve() + .body(AddressSearchResponse.class); + + if (addressSearchResponse.getMeta().getTotalCount() >= 2) { + throw new IllegalArgumentException("주소가 모호합니다. 정확한 주소를 입력하세요"); + } + + return Address.builder() + .longitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLongitude())) + .latitude(Double.parseDouble(addressSearchResponse.getDocuments().getFirst().getLatitude())) + .lotNumberAddress(addressSearchResponse.getDocuments().getFirst().getAddress().getAddressName()) + .roadAddress(addressSearchResponse.getDocuments().getFirst().getRoadAddress().getAddressName()) + .detailAddress(requestDto.getDetailAddress()) + .build(); + } + + @Data + @AllArgsConstructor + static class AddressSearchResponse { + private Meta meta; + private List documents; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Meta { + private Integer totalCount; + private Integer pageableCount; + private Boolean isEnd; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class Document { + private String addressName; + + private String addressType; + + @JsonProperty("x") + private String longitude; + + @JsonProperty("y") + private String latitude; + + private LotAddress address; + + private RoadAddress roadAddress; + + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class LotAddress { + private String addressName; + + @JsonProperty("region_1depth_name") + private String region1depthName; + + @JsonProperty("region_2depth_name") + private String region2depthName; + + @JsonProperty("region_3depth_name") + private String region3depthName; + + @JsonProperty("region_3depth_h_name") + private String region3depthHName; + + private String hCode; + + private String bCode; + + private String mountainYn; + + private String mainAddressNo; + + private String subAddressNo; + + private String x; + private String y; + } + + @Data + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + static class RoadAddress { + private String addressName; + + @JsonProperty("region_1depth_name") + private String region1depthName; + + @JsonProperty("region_2depth_name") + private String region2depthName; + + @JsonProperty("region_3depth_name") + private String region3depthName; + + private String roadName; + + @JsonProperty("underground_yn") + private String undergroundYn; + + @JsonProperty("main_building_no") + private String mainBuildingNo; + + @JsonProperty("sub_building_no") + private String subBuildingNo; + + @JsonProperty("building_name") + private String buildingName; + + @JsonProperty("zone_no") + private String zoneNo; + + private String x; + private String y; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java index d97cbdb..16f2761 100644 --- a/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java +++ b/src/main/java/com/stcom/smartmealtable/infrastructure/dto/AddressRequest.java @@ -1,6 +1,5 @@ package com.stcom.smartmealtable.infrastructure.dto; -import com.stcom.smartmealtable.domain.Address.AddressType; import lombok.AllArgsConstructor; import lombok.Data; @@ -10,8 +9,6 @@ public class AddressRequest { private String roadAddress; - private AddressType addressType; - private String alias; private String detailAddress; diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java index af3655b..85dd16a 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java @@ -3,7 +3,7 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.infrastructure.AddressApiService; +import com.stcom.smartmealtable.infrastructure.KakaoAddressApiService; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.security.JwtTokenService; import com.stcom.smartmealtable.service.BudgetService; @@ -40,7 +40,7 @@ public class MemberController { private final MemberService memberService; private final JwtTokenService jwtTokenService; - private final AddressApiService addressApiService; + private final KakaoAddressApiService addressApiService; private final BudgetService budgetService; private final SocialAccountService socialAccountService; private final FoodPreferenceService foodPreferenceService; From eb25d0933d7aa7f4061fe615cd972c32e7eec857 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 22:47:21 +0900 Subject: [PATCH 089/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MemberProfileService.java | 15 +++++++++++ .../controller/MemberProfileController.java | 25 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index 0307e64..ce33f84 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -1,6 +1,8 @@ package com.stcom.smartmealtable.service; +import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; import com.stcom.smartmealtable.domain.group.Group; import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.domain.member.MemberProfile; @@ -69,4 +71,17 @@ public void changeAddressToPrimary(Long profileId, Long addressId) { .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); profile.setPrimaryAddress(targetAddressEntity); } + + @Transactional + public void saveNewAddress(Long profileId, Address address, String alias, AddressType addressType) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + AddressEntity addressEntity = AddressEntity.builder() + .address(address) + .alias(alias) + .type(addressType) + .build(); + addressEntityRepository.save(addressEntity); + profile.addAddress(addressEntity); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 454bf84..c06d6b7 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -1,8 +1,11 @@ package com.stcom.smartmealtable.web.controller; import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressType; import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.infrastructure.AddressApiService; +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; import com.stcom.smartmealtable.service.MemberProfileService; import com.stcom.smartmealtable.service.dto.MemberDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; @@ -25,6 +28,7 @@ public class MemberProfileController { private final MemberProfileService memberProfileService; + private final AddressApiService addressApiService; @GetMapping("/me") public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { @@ -54,6 +58,14 @@ public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @Pa return ApiResponse.createSuccessWithNoContent(); } + @PostMapping("/addresses") + public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressCURequest request) { + Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); + memberProfileService.saveNewAddress(memberDto.getProfileId(), address, request.getAlias(), + request.getAddressType()); + return ApiResponse.createSuccessWithNoContent(); + } + @AllArgsConstructor @Data static class MemberProfilePageResponse { @@ -81,4 +93,17 @@ static class MemberProfileRequest { private MemberType memberType; } + @AllArgsConstructor + @Data + static class AddressCURequest { + private String roadAddress; + private AddressType addressType; + private String alias; + private String detailAddress; + + public AddressRequest toAddressApiRequest() { + return new AddressRequest(roadAddress, alias, detailAddress); + } + } + } From c5ddbdd18053e848b532cf7a99f4163cce64cf0d Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 23:02:32 +0900 Subject: [PATCH 090/120] =?UTF-8?q?feat:=20=EC=A3=BC=EC=86=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/Address/AddressEntity.java | 3 +++ .../smartmealtable/domain/member/MemberProfile.java | 12 ++++++++++++ .../smartmealtable/service/MemberProfileService.java | 10 ++++++++++ .../web/controller/MemberProfileController.java | 9 +++++++++ 4 files changed, 34 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java index 94877e2..4c34f96 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java @@ -66,4 +66,7 @@ public void changeAlias(String newAlias) { } + public void changeAddress(Address newAddress) { + this.address = newAddress; + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index d7cb93c..4ac7eb6 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -1,6 +1,8 @@ package com.stcom.smartmealtable.domain.member; +import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.group.Group; import jakarta.persistence.CascadeType; @@ -82,6 +84,16 @@ public void setPrimaryAddress(AddressEntity target) { target.markPrimary(); } + public void changeAddress(AddressEntity target, Address address, String alias, + AddressType addressType) { + AddressEntity entity = addressHistory.stream() + .filter(addressEntity -> addressEntity.equals(target)) + .findFirst().orElseThrow(() -> new IllegalStateException("해당 회원의 주소 엔티티가 아닙니다")); + entity.changeAddressType(addressType); + entity.changeAlias(alias); + entity.changeAddress(address); + } + public void linkMember(Member member) { member.linkMemberProfile(this); this.member = member; diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index ce33f84..d293b1b 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -84,4 +84,14 @@ public void saveNewAddress(Long profileId, Address address, String alias, Addres addressEntityRepository.save(addressEntity); profile.addAddress(addressEntity); } + + @Transactional + public void changeAddress(Long profileId, Long addressEntityId, Address address, String alias, + AddressType addressType) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + AddressEntity addressEntity = addressEntityRepository.findById(addressEntityId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); + profile.changeAddress(addressEntity, address, alias, addressType); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index c06d6b7..04699cd 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -66,6 +66,15 @@ public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressC return ApiResponse.createSuccessWithNoContent(); } + @PatchMapping("/addresses/{id}") + public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId, + AddressCURequest request) { + Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); + memberProfileService.changeAddress(memberDto.getProfileId(), addressId, address, request.getAlias(), + request.getAddressType()); + return ApiResponse.createSuccessWithNoContent(); + } + @AllArgsConstructor @Data static class MemberProfilePageResponse { From 897e5941755f9284857c6152ae1153e703f340e2 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Wed, 21 May 2025 23:13:20 +0900 Subject: [PATCH 091/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/member/MemberProfile.java | 13 ++++++++++++- .../service/MemberProfileService.java | 8 ++++++++ .../web/controller/MemberProfileController.java | 7 +++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 4ac7eb6..3e4a829 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -69,7 +69,18 @@ public void addAddress(AddressEntity addressEntity) { } public void removeAddress(AddressEntity addressEntity) { - addressHistory.remove(addressEntity); + boolean wasPrimary = addressEntity.isPrimaryAddress(); + addressHistory.stream() + .filter(a -> a.equals(addressEntity)) + .findFirst().ifPresentOrElse( + a -> addressHistory.remove(a), + () -> { + throw new IllegalStateException("Primary Address가 없습니다"); + } + ); + if (addressHistory.size() > 1 && wasPrimary) { + setPrimaryAddress(addressHistory.getFirst()); + } } public AddressEntity findPrimaryAddress() { diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index d293b1b..44eb977 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -94,4 +94,12 @@ public void changeAddress(Long profileId, Long addressEntityId, Address address, .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); profile.changeAddress(addressEntity, address, alias, addressType); } + + public void deleteAddress(Long profileId, Long addressEntityId) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + AddressEntity addressEntity = addressEntityRepository.findById(addressEntityId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); + profile.removeAddress(addressEntity); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 04699cd..03efdd8 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -14,6 +14,7 @@ import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -75,6 +76,12 @@ public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVaria return ApiResponse.createSuccessWithNoContent(); } + @DeleteMapping("/addresses/{id}") + public ApiResponse deleteAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { + memberProfileService.deleteAddress(memberDto.getProfileId(), addressId); + return ApiResponse.createSuccessWithNoContent(); + } + @AllArgsConstructor @Data static class MemberProfilePageResponse { From 84a662f70ec44a1cda1142eb687b60e38796969d Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 00:11:14 +0900 Subject: [PATCH 092/120] =?UTF-8?q?fix:=20JPA=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 예약어와 겹쳤던 칼럼명 수정 2. Enum에 값이 비어서 테이블 생성이 안되던 오류 수정 --- .../domain/Address/AddressEntity.java | 1 + .../smartmealtable/domain/Budget/Budget.java | 3 ++- .../stcom/smartmealtable/domain/group/Group.java | 6 ++++-- .../domain/group/IndustryType.java | 2 +- .../domain/member/MemberProfile.java | 4 ++-- .../repository/BudgetRepository.java | 16 ++++++++-------- .../repository/FoodPreferenceRepository.java | 6 ++++-- .../smartmealtable/service/BudgetService.java | 8 ++++---- .../service/FoodPreferenceService.java | 5 ++--- .../smartmealtable/service/dto/GroupDto.java | 14 -------------- 10 files changed, 28 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java index 4c34f96..e7e138a 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Address/AddressEntity.java @@ -27,6 +27,7 @@ public class AddressEntity { @Embedded private Address address; + @Column(name = "is_primary") private boolean primary = false; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java index d0995a7..7dc44de 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java +++ b/src/main/java/com/stcom/smartmealtable/domain/Budget/Budget.java @@ -7,6 +7,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Inheritance; import jakarta.persistence.InheritanceType; @@ -24,7 +25,7 @@ public abstract class Budget extends BaseTimeEntity { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "budget_id") private Long id; diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java index 90491cb..11cbae2 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java @@ -1,6 +1,7 @@ package com.stcom.smartmealtable.domain.group; import com.stcom.smartmealtable.domain.Address.Address; +import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorColumn; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -14,7 +15,7 @@ import lombok.NoArgsConstructor; @Entity -@Table(name = "groups") +@Table(name = "affiliation") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn @Getter @@ -23,11 +24,12 @@ public abstract class Group { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "affiliation_id") private Long id; @Embedded private Address address; private String name; - + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java index f641ca0..33d55dd 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java @@ -1,5 +1,5 @@ package com.stcom.smartmealtable.domain.group; public enum IndustryType { - + IT, FINANCE, MANUFACTURING, SERVICE } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index 3e4a829..a97db39 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -40,14 +40,14 @@ public class MemberProfile extends BaseTimeEntity { private String nickName; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_profile_id") private List addressHistory = new ArrayList<>(); @Embedded private MemberType type; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "group_id") + @JoinColumn(name = "affiliation_id") private Group group; diff --git a/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java b/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java index 4c7f86b..b9bc10b 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/BudgetRepository.java @@ -11,15 +11,15 @@ public interface BudgetRepository extends JpaRepository { - @Query("select b from Budget b where type(b) = DailyBudget and b.member.id = :memberId") - List findDailyBudgetsViaType(@Param("memberId") Long memberId); + @Query("select b from Budget b where type(b) = DailyBudget and b.memberProfile.id = :memberProfileId") + List findDailyBudgetsViaType(@Param("memberProfileId") Long memberProfileId); - @Query("select b from Budget b where type(b) = DailyBudget and b.member.id = :memberId order by treat (b as DailyBudget).date desc") - Optional findFirstDailyBudgetByMemberId(@Param("memberId") Long memberId); + @Query("select b from Budget b where type(b) = DailyBudget and b.memberProfile.id = :memberProfileId order by treat(b as DailyBudget).date desc") + Optional findFirstDailyBudgetByMemberProfileId(@Param("memberProfileId") Long memberProfileId); - @Query("select b from Budget b where type(b) = MonthlyBudget and b.member.id = :memberId") - List findMonthlyBudgetsViaType(@Param("memberId") Long memberId); + @Query("select b from Budget b where type(b) = MonthlyBudget and b.memberProfile.id = :memberProfileId") + List findMonthlyBudgetsViaType(@Param("memberProfileId") Long memberProfileId); - @Query("select b from Budget b where type(b) = MonthlyBudget and b.member.id = :memberId order by treat (b as MonthlyBudget ).yearMonth desc") - Optional findFirstMonthlyBudgetByMemberId(@Param("memberId") Long memberId); + @Query("select b from Budget b where type(b) = MonthlyBudget and b.memberProfile.id = :memberProfileId order by treat(b as MonthlyBudget).yearMonth desc") + Optional findFirstMonthlyBudgetByMemberProfileId(@Param("memberProfileId") Long memberProfileId); } diff --git a/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java b/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java index 82263a8..83bd74a 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java @@ -1,11 +1,13 @@ package com.stcom.smartmealtable.repository; import com.stcom.smartmealtable.domain.food.FoodPreference; -import com.stcom.smartmealtable.domain.member.Member; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface FoodPreferenceRepository extends JpaRepository { - List findFoodPreferencesByMember(Member member); + @Query("select fp from FoodPreference fp where fp.memberProfile.id = :memberProfileId") + List findFoodPreferencesByMemberProfileId(@Param("memberProfileId") Long memberProfileId); } diff --git a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java index 08263ab..a1c4633 100644 --- a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java +++ b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java @@ -14,12 +14,12 @@ public class BudgetService { private final BudgetRepository budgetRepository; - public DailyBudget findRecentDailyBudgetByMemberId(Long memberId) { - return budgetRepository.findFirstDailyBudgetByMemberId(memberId).orElse(null); + public DailyBudget findRecentDailyBudgetByMemberProfileId(Long memberProfileId) { + return budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId).orElse(null); } - public MonthlyBudget findRecentMonthlyBudgetByMemberId(Long memberId) { - return budgetRepository.findFirstMonthlyBudgetByMemberId(memberId).orElse(null); + public MonthlyBudget findRecentMonthlyBudgetByMemberProfileId(Long memberProfileId) { + return budgetRepository.findFirstMonthlyBudgetByMemberProfileId(memberProfileId).orElse(null); } } diff --git a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java index 94d5e48..b51887f 100644 --- a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java +++ b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java @@ -2,7 +2,6 @@ import com.stcom.smartmealtable.domain.food.FoodCategory; import com.stcom.smartmealtable.domain.food.FoodPreference; -import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.repository.FoodPreferenceRepository; import java.util.List; import lombok.RequiredArgsConstructor; @@ -16,8 +15,8 @@ public class FoodPreferenceService { private final FoodPreferenceRepository foodPreferenceRepository; - public List findPreferredFoodCategories(Member member) { - return foodPreferenceRepository.findFoodPreferencesByMember(member) + public List findPreferredFoodCategories(Long memberProfileId) { + return foodPreferenceRepository.findFoodPreferencesByMemberProfileId(memberProfileId) .stream() .map(FoodPreference::getCategory) .toList(); diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java deleted file mode 100644 index 0653ca4..0000000 --- a/src/main/java/com/stcom/smartmealtable/service/dto/GroupDto.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.stcom.smartmealtable.service.dto; - -import com.stcom.smartmealtable.domain.group.GroupType; -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class GroupDto { - - private Long id; - private GroupType groupType; - private String address; -} From e53236988c40a6b9d7b10ca1ced6243a786fe74a Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 01:26:50 +0900 Subject: [PATCH 093/120] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/group/CompanyGroup.java | 5 +++ .../smartmealtable/domain/group/Group.java | 2 ++ .../domain/group/IndustryType.java | 12 ++++++- .../domain/group/SchoolGroup.java | 5 +++ .../domain/group/SchoolType.java | 13 +++++++- .../repository/GroupRepository.java | 3 ++ .../smartmealtable/service/GroupService.java | 6 ++++ .../web/controller/GroupController.java | 32 +++++++++++++++++++ 8 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java b/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java index 8803f07..8aee3e1 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/CompanyGroup.java @@ -13,4 +13,9 @@ public class CompanyGroup extends Group { @Enumerated(EnumType.STRING) private IndustryType industryType; + + @Override + public String getTypeName() { + return industryType.getDescription(); + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java index 11cbae2..d221901 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/Group.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/Group.java @@ -32,4 +32,6 @@ public abstract class Group { private String name; + public abstract String getTypeName(); + } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java index 33d55dd..f8e39ac 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/IndustryType.java @@ -1,5 +1,15 @@ package com.stcom.smartmealtable.domain.group; public enum IndustryType { - IT, FINANCE, MANUFACTURING, SERVICE + IT("IT"), FINANCE("파이낸스"), MANUFACTURING("제조업"), SERVICE("서비스"); + + private final String description; + + IndustryType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java index 1c3a2aa..17b1858 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolGroup.java @@ -13,4 +13,9 @@ public class SchoolGroup extends Group { @Enumerated(EnumType.STRING) private SchoolType schoolType; + + @Override + public String getTypeName() { + return schoolType.name(); + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java index 51023b1..965fef4 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/group/SchoolType.java @@ -1,5 +1,16 @@ package com.stcom.smartmealtable.domain.group; public enum SchoolType { - UNIVERSITY_FOUR_YEAR, UNIVERSITY_TWO_YEAR, HIGH_SCHOOL, MIDDLE_SCHOOL + UNIVERSITY_FOUR_YEAR("대학교(4년제)"), UNIVERSITY_TWO_YEAR("대학교(2년제)"), + HIGH_SCHOOL("고등학교"), MIDDLE_SCHOOL("중학교"); + + private final String description; + + SchoolType(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } } diff --git a/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java b/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java index 443788f..5424251 100644 --- a/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java +++ b/src/main/java/com/stcom/smartmealtable/repository/GroupRepository.java @@ -1,8 +1,11 @@ package com.stcom.smartmealtable.repository; import com.stcom.smartmealtable.domain.group.Group; +import java.util.List; +import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; public interface GroupRepository extends JpaRepository { + List findByNameContaining(String name, Limit limit); } diff --git a/src/main/java/com/stcom/smartmealtable/service/GroupService.java b/src/main/java/com/stcom/smartmealtable/service/GroupService.java index 90f0783..394fcc0 100644 --- a/src/main/java/com/stcom/smartmealtable/service/GroupService.java +++ b/src/main/java/com/stcom/smartmealtable/service/GroupService.java @@ -2,7 +2,9 @@ import com.stcom.smartmealtable.domain.group.Group; import com.stcom.smartmealtable.repository.GroupRepository; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Limit; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,4 +19,8 @@ public Group findGroupByGroupId(Long groupId) { return groupRepository.findById(groupId) .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); } + + public List findGroupsByKeyword(String keyword) { + return groupRepository.findByNameContaining(keyword, Limit.of(10)); + } } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java index 225b9ae..edd8098 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java @@ -1,8 +1,15 @@ package com.stcom.smartmealtable.web.controller; +import com.stcom.smartmealtable.domain.group.Group; import com.stcom.smartmealtable.service.GroupService; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -11,4 +18,29 @@ public class GroupController { private final GroupService groupService; + + @GetMapping() + public ApiResponse searchGroup(@RequestParam String keyword) { + if (keyword.isBlank()) { + return ApiResponse.createError("키워드가 비어있습니다. 키워드를 입력해주세요"); + } + List result = groupService.findGroupsByKeyword(keyword); + return ApiResponse.createSuccess(result.stream() + .map(GroupDto::new) + .toList()); + } + + @Data + @AllArgsConstructor + static class GroupDto { + private String roadAddress; + private String name; + private String groupType; + + public GroupDto(Group group) { + this.groupType = group.getTypeName(); + this.name = group.getName(); + this.roadAddress = group.getAddress().getRoadAddress(); + } + } } From 2ae47b9ea93db4144c4674012c724254165c9e00 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 01:27:07 +0900 Subject: [PATCH 094/120] =?UTF-8?q?fix:=20Api=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EC=99=80=20=EB=8B=A4=EB=A5=B8=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/web/controller/MemberProfileController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 03efdd8..fb9f6be 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -45,7 +45,7 @@ public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, return ApiResponse.createSuccessWithNoContent(); } - @PatchMapping + @PatchMapping("/me") public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, @Validated @RequestBody MemberProfileRequest request) { memberProfileService.changeProfile(memberDto.getProfileId(), request.getNickName(), request.getMemberType(), From 3cdafa90222f3d07371a526bb4c16463c19a83a9 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 01:54:46 +0900 Subject: [PATCH 095/120] =?UTF-8?q?feat:=20Term=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/domain/term/Term.java | 32 ++++++++++ .../domain/term/TermAgreement.java | 41 ++++++++++++ .../repository/TermAgreementRepository.java | 7 +++ .../repository/TermRepository.java | 7 +++ .../smartmealtable/service/TermService.java | 62 +++++++++++++++++++ .../service/dto/TermAgreementRequestDto.java | 11 ++++ .../web/controller/TermController.java | 46 ++++++++++++++ 7 files changed, 206 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/term/Term.java create mode 100644 src/main/java/com/stcom/smartmealtable/domain/term/TermAgreement.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/TermAgreementRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/TermRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/TermService.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/dto/TermAgreementRequestDto.java create mode 100644 src/main/java/com/stcom/smartmealtable/web/controller/TermController.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/term/Term.java b/src/main/java/com/stcom/smartmealtable/domain/term/Term.java new file mode 100644 index 0000000..14c809c --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/term/Term.java @@ -0,0 +1,32 @@ +package com.stcom.smartmealtable.domain.term; + +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Term extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "term_id") + private Long id; + + private Integer version; + + private String title; + + @Lob + private String content; + + private Boolean isRequired; + +} diff --git a/src/main/java/com/stcom/smartmealtable/domain/term/TermAgreement.java b/src/main/java/com/stcom/smartmealtable/domain/term/TermAgreement.java new file mode 100644 index 0000000..a932ee7 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/term/TermAgreement.java @@ -0,0 +1,41 @@ +package com.stcom.smartmealtable.domain.term; + +import com.stcom.smartmealtable.domain.common.BaseTimeEntity; +import com.stcom.smartmealtable.domain.member.Member; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class TermAgreement extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "term_id") + private Term term; + + private Boolean isAgreed; + + @Builder + public TermAgreement(Member member, Term term, Boolean isAgreed) { + this.member = member; + this.term = term; + this.isAgreed = isAgreed; + } +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/TermAgreementRepository.java b/src/main/java/com/stcom/smartmealtable/repository/TermAgreementRepository.java new file mode 100644 index 0000000..7f4e06f --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/TermAgreementRepository.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.term.TermAgreement; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TermAgreementRepository extends JpaRepository { +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/TermRepository.java b/src/main/java/com/stcom/smartmealtable/repository/TermRepository.java new file mode 100644 index 0000000..4a4fe2a --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/TermRepository.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.term.Term; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TermRepository extends JpaRepository { +} diff --git a/src/main/java/com/stcom/smartmealtable/service/TermService.java b/src/main/java/com/stcom/smartmealtable/service/TermService.java new file mode 100644 index 0000000..6e8e401 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/TermService.java @@ -0,0 +1,62 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.domain.term.TermAgreement; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.TermAgreementRepository; +import com.stcom.smartmealtable.repository.TermRepository; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class TermService { + + private final TermRepository termRepository; + private final MemberRepository memberRepository; + private final TermAgreementRepository termAgreementRepository; + + public List findAll() { + return termRepository.findAll(); + } + + + public void agreeTerms(Long memberId, List termAgreements) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); + + List requiredTerms = termRepository.findAll().stream() + .filter(Term::getIsRequired) + .toList(); + + Map agreementMap = termAgreements.stream() + .collect(Collectors.toMap(TermAgreementRequestDto::getTermId, + TermAgreementRequestDto::getIsAgreed)); + + for (Term term : requiredTerms) { + Boolean agreed = agreementMap.get(term.getId()); + if (agreed == null || !agreed) { + throw new IllegalArgumentException("필수 약관에 동의해야 합니다: " + term.getTitle()); + } + } + + // 약관 동의 저장 + for (TermAgreementRequestDto dto : termAgreements) { + Term term = termRepository.findById(dto.getTermId()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 약관입니다: " + dto.getTermId())); + TermAgreement agreement = TermAgreement.builder() + .member(member) + .term(term) + .isAgreed(dto.getIsAgreed()) + .build(); + termAgreementRepository.save(agreement); + } + } +} diff --git a/src/main/java/com/stcom/smartmealtable/service/dto/TermAgreementRequestDto.java b/src/main/java/com/stcom/smartmealtable/service/dto/TermAgreementRequestDto.java new file mode 100644 index 0000000..0a5583d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/dto/TermAgreementRequestDto.java @@ -0,0 +1,11 @@ +package com.stcom.smartmealtable.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class TermAgreementRequestDto { + private Long termId; + private Boolean isAgreed; +} diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java b/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java new file mode 100644 index 0000000..4f27fda --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java @@ -0,0 +1,46 @@ +package com.stcom.smartmealtable.web.controller; + +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.service.TermService; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/terms") +public class TermController { + + private final TermService termService; + + @GetMapping() + public ApiResponse getTerms() { + List result = termService.findAll(); + return ApiResponse.createSuccess(result.stream() + .map(TermResponse::new) + .toList()); + } + + @Data + @AllArgsConstructor + static class TermResponse { + private Long termId; + private String title; + private String content; + private boolean isRequired; + + public TermResponse(Term term) { + this.termId = term.getId(); + this.title = term.getTitle(); + this.content = term.getContent(); + this.isRequired = term.getIsRequired(); + } + + } + +} From 8bff8bb1a07f761355751c6230068cb2d2455d33 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 01:54:57 +0900 Subject: [PATCH 096/120] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=B5=9C=EC=A2=85=20=EC=99=84=EB=A3=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/MemberController.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java index 85dd16a..3e3e57b 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java @@ -3,18 +3,17 @@ import com.stcom.smartmealtable.domain.member.Member; import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.exception.PasswordPolicyException; -import com.stcom.smartmealtable.infrastructure.KakaoAddressApiService; import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; import com.stcom.smartmealtable.security.JwtTokenService; -import com.stcom.smartmealtable.service.BudgetService; -import com.stcom.smartmealtable.service.FoodPreferenceService; import com.stcom.smartmealtable.service.MemberService; -import com.stcom.smartmealtable.service.SocialAccountService; +import com.stcom.smartmealtable.service.TermService; import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; import com.stcom.smartmealtable.web.dto.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -40,10 +39,7 @@ public class MemberController { private final MemberService memberService; private final JwtTokenService jwtTokenService; - private final KakaoAddressApiService addressApiService; - private final BudgetService budgetService; - private final SocialAccountService socialAccountService; - private final FoodPreferenceService foodPreferenceService; + private final TermService termService; @GetMapping("/email/check") public ResponseEntity> checkEmail(@Email @RequestParam String email) { @@ -85,6 +81,23 @@ public ApiResponse deleteMember(@UserContext MemberDto memberDto) { return ApiResponse.createSuccessWithNoContent(); } + @PostMapping("/signup") + public ApiResponse signUpWithTermAgreement(@UserContext MemberDto memberDto, + @RequestBody List agreements) { + termService.agreeTerms( + memberDto.getMemberId(), + agreements.stream() + .map(dto -> new TermAgreementRequestDto(dto.getTermId(), dto.getIsAgreed())) + .toList() + ); + return ApiResponse.createSuccessWithNoContent(); + } + + @DeleteMapping("/signup") + public ApiResponse signUpCancel(@UserContext MemberDto memberDto) { + memberService.deleteByMemberId(memberDto.getMemberId()); + return ApiResponse.createSuccessWithNoContent(); + } @Data @AllArgsConstructor @@ -106,4 +119,11 @@ static class EditMemberRequest { private String confirmPassword; } + @Data + @AllArgsConstructor + static class TermAgreementDto { + private Long termId; + private Boolean isAgreed; + } + } From 32045a2cb80b963aa05ab5886574d9a85645b027 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 12:44:51 +0900 Subject: [PATCH 097/120] =?UTF-8?q?refactor:=20=EC=9D=8C=EC=8B=9D=20?= =?UTF-8?q?=EC=84=A0=ED=98=B8=EB=8F=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/food/FoodCategory.java | 22 ++++++- ...nce.java => MemberCategoryPreference.java} | 36 +++++------ .../domain/food/PreferenceType.java | 5 ++ .../repository/FoodCategoryRepository.java | 7 +++ .../repository/FoodPreferenceRepository.java | 13 ---- .../MemberCategoryPreferenceRepository.java | 14 +++++ .../service/FoodPreferenceService.java | 24 -------- .../MemberCategoryPreferenceService.java | 61 +++++++++++++++++++ 8 files changed, 126 insertions(+), 56 deletions(-) rename src/main/java/com/stcom/smartmealtable/domain/food/{FoodPreference.java => MemberCategoryPreference.java} (60%) create mode 100644 src/main/java/com/stcom/smartmealtable/domain/food/PreferenceType.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/FoodCategoryRepository.java delete mode 100644 src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java create mode 100644 src/main/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepository.java delete mode 100644 src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java create mode 100644 src/main/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceService.java diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java b/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java index 8ed328e..d3075fd 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java +++ b/src/main/java/com/stcom/smartmealtable/domain/food/FoodCategory.java @@ -1,5 +1,23 @@ package com.stcom.smartmealtable.domain.food; -public enum FoodCategory { - KOREAN, JAPANESE, WESTERN, CHINESE +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor +@Getter +public class FoodCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "food_category_id") + private Long id; + + @Column(unique = true, nullable = false) + private String name; } diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java b/src/main/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreference.java similarity index 60% rename from src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java rename to src/main/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreference.java index 665f466..0b4458d 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/food/FoodPreference.java +++ b/src/main/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreference.java @@ -1,6 +1,5 @@ package com.stcom.smartmealtable.domain.food; -import com.stcom.smartmealtable.domain.common.BaseTimeEntity; import com.stcom.smartmealtable.domain.member.MemberProfile; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -12,44 +11,47 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity -@Getter +@Table @NoArgsConstructor -public class FoodPreference extends BaseTimeEntity { +@Getter +public class MemberCategoryPreference { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "food_preference_id") + @Column(name = "member_category_preference_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_profile_id") private MemberProfile memberProfile; - @Enumerated(EnumType.STRING) - @Column(name = "category") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "food_category_id") private FoodCategory category; - private boolean isPreferred; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PreferenceType type; // LIKE or DISLIKE + + @Column(nullable = false) + private Integer priority; private Double weight; @Builder - public FoodPreference(MemberProfile memberProfile, FoodCategory category, boolean isPreferred, Double weight) { + public MemberCategoryPreference(MemberProfile memberProfile, + FoodCategory category, + PreferenceType type, + Integer priority) { this.memberProfile = memberProfile; this.category = category; - this.isPreferred = isPreferred; - this.weight = weight; + this.type = type; + this.priority = priority; } - - - public void updatePreference(boolean isPreferred, Double weight) { - this.isPreferred = isPreferred; - this.weight = weight; - } - } diff --git a/src/main/java/com/stcom/smartmealtable/domain/food/PreferenceType.java b/src/main/java/com/stcom/smartmealtable/domain/food/PreferenceType.java new file mode 100644 index 0000000..0ba42bf --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/domain/food/PreferenceType.java @@ -0,0 +1,5 @@ +package com.stcom.smartmealtable.domain.food; + +public enum PreferenceType { + LIKE, DISLIKE +} diff --git a/src/main/java/com/stcom/smartmealtable/repository/FoodCategoryRepository.java b/src/main/java/com/stcom/smartmealtable/repository/FoodCategoryRepository.java new file mode 100644 index 0000000..5ad9678 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/FoodCategoryRepository.java @@ -0,0 +1,7 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FoodCategoryRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java b/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java deleted file mode 100644 index 83bd74a..0000000 --- a/src/main/java/com/stcom/smartmealtable/repository/FoodPreferenceRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.stcom.smartmealtable.repository; - -import com.stcom.smartmealtable.domain.food.FoodPreference; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface FoodPreferenceRepository extends JpaRepository { - - @Query("select fp from FoodPreference fp where fp.memberProfile.id = :memberProfileId") - List findFoodPreferencesByMemberProfileId(@Param("memberProfileId") Long memberProfileId); -} diff --git a/src/main/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepository.java b/src/main/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepository.java new file mode 100644 index 0000000..1688aa3 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/repository/MemberCategoryPreferenceRepository.java @@ -0,0 +1,14 @@ +package com.stcom.smartmealtable.repository; + +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface MemberCategoryPreferenceRepository extends JpaRepository { + + @Query("select mcp from MemberCategoryPreference mcp where mcp.memberProfile.id = :memberProfileId order by mcp.priority asc") + List findDefaultByMemberProfileId(Long memberProfileId); + + void deleteByMemberProfile_Id(Long memberProfileId); +} \ No newline at end of file diff --git a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java b/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java deleted file mode 100644 index b51887f..0000000 --- a/src/main/java/com/stcom/smartmealtable/service/FoodPreferenceService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.stcom.smartmealtable.service; - -import com.stcom.smartmealtable.domain.food.FoodCategory; -import com.stcom.smartmealtable.domain.food.FoodPreference; -import com.stcom.smartmealtable.repository.FoodPreferenceRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class FoodPreferenceService { - - private final FoodPreferenceRepository foodPreferenceRepository; - - public List findPreferredFoodCategories(Long memberProfileId) { - return foodPreferenceRepository.findFoodPreferencesByMemberProfileId(memberProfileId) - .stream() - .map(FoodPreference::getCategory) - .toList(); - } -} diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceService.java b/src/main/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceService.java new file mode 100644 index 0000000..f028d68 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceService.java @@ -0,0 +1,61 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.FoodCategoryRepository; +import com.stcom.smartmealtable.repository.MemberCategoryPreferenceRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MemberCategoryPreferenceService { + + private final MemberCategoryPreferenceRepository preferenceRepository; + private final FoodCategoryRepository categoryRepository; + private final MemberProfileRepository profileRepository; + + @Transactional + public void savePreferences(Long profileId, List liked, List disliked) { + MemberProfile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + + preferenceRepository.deleteByMemberProfile_Id(profileId); + + int priority = 1; + for (Long catId : liked) { + FoodCategory category = categoryRepository.findById(catId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카테고리입니다: " + catId)); + MemberCategoryPreference pref = MemberCategoryPreference.builder() + .memberProfile(profile) + .category(category) + .type(PreferenceType.LIKE) + .priority(priority++) + .build(); + preferenceRepository.save(pref); + } + + priority = 1; + for (Long catId : disliked) { + FoodCategory category = categoryRepository.findById(catId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 카테고리입니다: " + catId)); + MemberCategoryPreference pref = MemberCategoryPreference.builder() + .memberProfile(profile) + .category(category) + .type(PreferenceType.DISLIKE) + .priority(priority++) + .build(); + preferenceRepository.save(pref); + } + } + + public List getPreferences(Long profileId) { + return preferenceRepository.findDefaultByMemberProfileId(profileId); + } +} \ No newline at end of file From d61b6c5b5173c98e82b76a0e3fcf530cb08e237a Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 12:45:33 +0900 Subject: [PATCH 098/120] =?UTF-8?q?feat:=20Budget=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberProfile.java | 17 ++++++++++-- .../domain/member/MemberType.java | 3 --- .../smartmealtable/service/BudgetService.java | 27 +++++++++++++++++-- .../service/MemberProfileService.java | 9 +++++++ 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java index a97db39..48e7af4 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberProfile.java @@ -7,8 +7,9 @@ import com.stcom.smartmealtable.domain.group.Group; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; -import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,6 +18,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import lombok.Builder; @@ -43,13 +45,19 @@ public class MemberProfile extends BaseTimeEntity { @JoinColumn(name = "member_profile_id") private List addressHistory = new ArrayList<>(); - @Embedded + @Enumerated(EnumType.STRING) private MemberType type; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "affiliation_id") private Group group; + @Column(name = "default_monthly_limit") + private BigDecimal defaultMonthlyLimit; + + @Column(name = "default_daily_limit") + private BigDecimal defaultDailyLimit; + @Builder public MemberProfile(Member member, String nickName, List addressHistory, MemberType type, @@ -125,4 +133,9 @@ public void changeMemberType(MemberType memberType) { public void changeGroup(Group newGroup) { this.group = newGroup; } + + public void registerDefaultBudgets(BigDecimal defaultDailyLimit, BigDecimal defaultMonthlyLimit) { + this.defaultDailyLimit = defaultDailyLimit; + this.defaultMonthlyLimit = defaultMonthlyLimit; + } } diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java index ce8cebf..f4d40f5 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberType.java @@ -1,8 +1,5 @@ package com.stcom.smartmealtable.domain.member; -import jakarta.persistence.Embeddable; - -@Embeddable public enum MemberType { STUDENT, WORKER, OTHER } diff --git a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java index a1c4633..14661cc 100644 --- a/src/main/java/com/stcom/smartmealtable/service/BudgetService.java +++ b/src/main/java/com/stcom/smartmealtable/service/BudgetService.java @@ -2,7 +2,12 @@ import com.stcom.smartmealtable.domain.Budget.DailyBudget; import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.repository.BudgetRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,13 +18,31 @@ public class BudgetService { private final BudgetRepository budgetRepository; + private final MemberProfileRepository memberProfileRepository; public DailyBudget findRecentDailyBudgetByMemberProfileId(Long memberProfileId) { - return budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId).orElse(null); + return budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필로 접근")); } public MonthlyBudget findRecentMonthlyBudgetByMemberProfileId(Long memberProfileId) { - return budgetRepository.findFirstMonthlyBudgetByMemberProfileId(memberProfileId).orElse(null); + return budgetRepository.findFirstMonthlyBudgetByMemberProfileId(memberProfileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필로 접근")); } + @Transactional + public void saveMonthlyBudgetCustom(Long memberProfileId, Long limit) { + MemberProfile profile = memberProfileRepository.findById(memberProfileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필로 접근")); + MonthlyBudget monthlyBudget = new MonthlyBudget(profile, BigDecimal.valueOf(limit), YearMonth.now()); + budgetRepository.save(monthlyBudget); + } + + @Transactional + public void saveDailyBudgetCustom(Long memberProfileId, Long limit) { + MemberProfile profile = memberProfileRepository.findById(memberProfileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필로 접근")); + DailyBudget dailyBudget = new DailyBudget(profile, BigDecimal.valueOf(limit), LocalDate.now()); + budgetRepository.save(dailyBudget); + } } diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java index 44eb977..8176c88 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberProfileService.java @@ -11,6 +11,7 @@ import com.stcom.smartmealtable.repository.GroupRepository; import com.stcom.smartmealtable.repository.MemberProfileRepository; import com.stcom.smartmealtable.repository.MemberRepository; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -95,6 +96,7 @@ public void changeAddress(Long profileId, Long addressEntityId, Address address, profile.changeAddress(addressEntity, address, alias, addressType); } + @Transactional public void deleteAddress(Long profileId, Long addressEntityId) { MemberProfile profile = memberProfileRepository.findById(profileId) .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); @@ -102,4 +104,11 @@ public void deleteAddress(Long profileId, Long addressEntityId) { .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 주소 정보입니다.")); profile.removeAddress(addressEntity); } + + @Transactional + public void registerDefaultBudgets(Long profileId, Long dailyLimit, Long monthlyLimit) { + MemberProfile profile = memberProfileRepository.findById(profileId) + .orElseThrow(() -> new IllegalStateException("존재하지 않는 프로필입니다")); + profile.registerDefaultBudgets(BigDecimal.valueOf(dailyLimit), BigDecimal.valueOf(monthlyLimit)); + } } From a3972c9d29ed4e9c9464b4a8a2a1b48fd3cebe11 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 12:45:47 +0900 Subject: [PATCH 099/120] =?UTF-8?q?feat:=20budget=20=EB=93=B1=EB=A1=9D=20a?= =?UTF-8?q?pi=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MemberProfileController.java | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index fb9f6be..1c84384 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -2,14 +2,19 @@ import com.stcom.smartmealtable.domain.Address.Address; import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; import com.stcom.smartmealtable.domain.member.MemberProfile; import com.stcom.smartmealtable.domain.member.MemberType; import com.stcom.smartmealtable.infrastructure.AddressApiService; import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import com.stcom.smartmealtable.service.BudgetService; +import com.stcom.smartmealtable.service.MemberCategoryPreferenceService; import com.stcom.smartmealtable.service.MemberProfileService; import com.stcom.smartmealtable.service.dto.MemberDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; import com.stcom.smartmealtable.web.dto.ApiResponse; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -30,6 +35,8 @@ public class MemberProfileController { private final MemberProfileService memberProfileService; private final AddressApiService addressApiService; + private final MemberCategoryPreferenceService memberCategoryPreferenceService; + private final BudgetService budgetService; @GetMapping("/me") public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { @@ -53,13 +60,13 @@ public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, return ApiResponse.createSuccessWithNoContent(); } - @PostMapping("/addresses/{id}/primary") + @PostMapping("/me/addresses/{id}/primary") public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); return ApiResponse.createSuccessWithNoContent(); } - @PostMapping("/addresses") + @PostMapping("/me/addresses") public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressCURequest request) { Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); memberProfileService.saveNewAddress(memberDto.getProfileId(), address, request.getAlias(), @@ -67,7 +74,7 @@ public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressC return ApiResponse.createSuccessWithNoContent(); } - @PatchMapping("/addresses/{id}") + @PatchMapping("/me/addresses/{id}") public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId, AddressCURequest request) { Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); @@ -76,12 +83,54 @@ public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVaria return ApiResponse.createSuccessWithNoContent(); } - @DeleteMapping("/addresses/{id}") + @DeleteMapping("/me/addresses/{id}") public ApiResponse deleteAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { memberProfileService.deleteAddress(memberDto.getProfileId(), addressId); return ApiResponse.createSuccessWithNoContent(); } + @GetMapping("/me/preferences") + public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { + List preferences = + memberCategoryPreferenceService.getPreferences(memberDto.getProfileId()); + + List liked = preferences.stream() + .filter(p -> p.getType() == PreferenceType.LIKE) + .map(p -> new CategoryPreferenceDto( + p.getCategory().getId(), + p.getCategory().getName(), + p.getPriority())) + .toList(); + + List disliked = preferences.stream() + .filter(p -> p.getType() == PreferenceType.DISLIKE) + .map(p -> new CategoryPreferenceDto( + p.getCategory().getId(), + p.getCategory().getName(), + p.getPriority())) + .toList(); + + return ApiResponse.createSuccess(new PreferencesResponse(liked, disliked)); + } + + @PostMapping("/me/preferences") + public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, + @RequestBody PreferencesRequest request) { + memberCategoryPreferenceService.savePreferences( + memberDto.getProfileId(), + request.getLiked(), + request.getDisliked()); + return ApiResponse.createSuccessWithNoContent(); + } + + @PostMapping("/me/budgets") + public ApiResponse createDefaultBudgets(@UserContext MemberDto memberDto, + @RequestBody BudgetRequest budgetRequest) { + memberProfileService.registerDefaultBudgets(memberDto.getProfileId(), budgetRequest.getDailyLimit(), + budgetRequest.getMonthlyLimit()); + return ApiResponse.createSuccessWithNoContent(); + } + @AllArgsConstructor @Data static class MemberProfilePageResponse { @@ -122,4 +171,33 @@ public AddressRequest toAddressApiRequest() { } } + @AllArgsConstructor + @Data + static class PreferencesRequest { + private List liked; + private List disliked; + } + + @AllArgsConstructor + @Data + static class PreferencesResponse { + private List liked; + private List disliked; + } + + @AllArgsConstructor + @Data + static class CategoryPreferenceDto { + private Long categoryId; + private String categoryName; + private Integer priority; + } + + @AllArgsConstructor + @Data + static class BudgetRequest { + private Long dailyLimit; + private Long monthlyLimit; + } + } From 4205386279f8568cbe65bf86277d93cf0cc236b4 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 12:45:59 +0900 Subject: [PATCH 100/120] =?UTF-8?q?chore:=20swagger=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../smartmealtable/config/SwaggerConfig.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 73c2032..fe17620 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화/역직렬화에 Jackson 사용 compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java new file mode 100644 index 0000000..3a834e0 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java @@ -0,0 +1,18 @@ +package com.stcom.smartmealtable.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; + +@Configuration +@Profile({"local","dev"}) +@OpenAPIDefinition( + info = @Info( + title = "SmartMealTable API 문서", + version = "v1", + description = "SmartMealTable API 명세서입니다." + ) +) +public class SwaggerConfig { +} \ No newline at end of file From 915b42c976be88a93c31384a49673ae2a4dc00f7 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 13:09:45 +0900 Subject: [PATCH 101/120] =?UTF-8?q?fix:=20api=20URI=20=EC=98=A4=ED=83=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/web/controller/OAuth2Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java b/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java index ee40581..d25461d 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/OAuth2Controller.java @@ -43,7 +43,7 @@ public ApiResponse getTokenFromSocial(@RequestBody JwtToken return ApiResponse.createSuccess(jwtDto); } - @PostMapping("/api/v1/auth/token/refresh") + @PostMapping("/token/refresh") public ApiResponse refreshAccessToken(@UserContext MemberDto memberDto, @RequestBody JwtRefreshTokenRequest request) { String accessToken = jwtTokenService.createAccessToken(memberDto.getMemberId(), memberDto.getProfileId()); From 2153a1401b9663303c28b1292d06cb820a49eff4 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 13:29:21 +0900 Subject: [PATCH 102/120] =?UTF-8?q?fix:=20ArgumentResolver=20=EB=8C=80?= =?UTF-8?q?=EC=83=81=20=EA=B0=9D=EC=B2=B4=20Swagger=20Docs=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/config/SwaggerConfig.java | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java index 3a834e0..2bd179f 100644 --- a/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java +++ b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java @@ -1,18 +1,53 @@ package com.stcom.smartmealtable.config; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; 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.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springdoc.core.utils.SpringDocUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.MethodParameter; @Configuration -@Profile({"local","dev"}) +@Profile({"local", "dev"}) +@SecurityScheme( + name = "bearerAuth", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT", + in = SecuritySchemeIn.HEADER +) @OpenAPIDefinition( - info = @Info( - title = "SmartMealTable API 문서", - version = "v1", - description = "SmartMealTable API 명세서입니다." - ) + security = @SecurityRequirement(name = "bearerAuth"), + info = @Info( + title = "SmartMealTable API 문서", + version = "v1", + description = "SmartMealTable API 명세서입니다." + ) ) public class SwaggerConfig { + + static { + SpringDocUtils.getConfig().addAnnotationsToIgnore(UserContext.class); + } + + @Bean + public OperationCustomizer userContextSecurityCustomizer() { + return (operation, handlerMethod) -> { + for (MethodParameter param : handlerMethod.getMethodParameters()) { + if (param.hasParameterAnnotation(UserContext.class)) { + operation.addSecurityItem( + new io.swagger.v3.oas.models.security.SecurityRequirement().addList("bearerAuth")); + break; + } + } + return operation; + }; + } } \ No newline at end of file From 4fa2a1ac9321d9b4b37498ce0f52768d7206fb3f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 15:29:07 +0900 Subject: [PATCH 103/120] =?UTF-8?q?fix:=20swagger=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20DTO=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/config/SwaggerConfig.java | 4 +++- .../web/controller/GroupController.java | 2 +- .../web/controller/LoginController.java | 4 ++-- .../web/controller/MemberController.java | 12 +++++------ .../controller/MemberProfileController.java | 20 +++++++++---------- .../web/controller/TermController.java | 2 +- .../smartmealtable/web/dto/ApiResponse.java | 6 +++--- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java index 2bd179f..167b891 100644 --- a/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java +++ b/src/main/java/com/stcom/smartmealtable/config/SwaggerConfig.java @@ -1,6 +1,7 @@ package com.stcom.smartmealtable.config; import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.dto.ApiResponse; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; @@ -34,7 +35,8 @@ public class SwaggerConfig { static { - SpringDocUtils.getConfig().addAnnotationsToIgnore(UserContext.class); + SpringDocUtils.getConfig().addAnnotationsToIgnore(UserContext.class) + .addResponseWrapperToIgnore(ApiResponse.class); } @Bean diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java index edd8098..b7e14fd 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/GroupController.java @@ -20,7 +20,7 @@ public class GroupController { private final GroupService groupService; @GetMapping() - public ApiResponse searchGroup(@RequestParam String keyword) { + public ApiResponse> searchGroup(@RequestParam String keyword) { if (keyword.isBlank()) { return ApiResponse.createError("키워드가 비어있습니다. 키워드를 입력해주세요"); } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java b/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java index b0af479..5a6103c 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/LoginController.java @@ -30,7 +30,7 @@ public class LoginController { private final JwtBlacklistService jwtBlacklistService; @PostMapping("/login") - public ApiResponse login(@Validated @RequestBody LoginRequest request) + public ApiResponse login(@Validated @RequestBody LoginRequest request) throws PasswordFailedExceededException { AuthResultDto authResultDto = loginService.loginWithEmail(request.getEmail(), request.getPassword()); JwtTokenResponseDto jwtTokenResponseDto = @@ -42,7 +42,7 @@ public ApiResponse login(@Validated @RequestBody LoginRequest request) } @PostMapping("/logout") - public ApiResponse logout(HttpServletRequest request) { + public ApiResponse logout(HttpServletRequest request) { String jwt = request.getHeader("Authorization"); jwtBlacklistService.addToBlacklist(jwt); return ApiResponse.createSuccessWithNoContent(); diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java index 3e3e57b..dd3053e 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberController.java @@ -42,14 +42,14 @@ public class MemberController { private final TermService termService; @GetMapping("/email/check") - public ResponseEntity> checkEmail(@Email @RequestParam String email) { + public ResponseEntity> checkEmail(@Email @RequestParam String email) { memberService.validateDuplicatedEmail(email); return ResponseEntity.ok().body(ApiResponse.createSuccessWithNoContent()); } @ResponseStatus(HttpStatus.CREATED) @PostMapping() - public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, + public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest request, BindingResult bindingResult) throws PasswordPolicyException { memberService.validateDuplicatedEmail(request.getEmail()); memberService.checkPasswordDoubly(request.getPassword(), request.getConfirmPassword()); @@ -67,7 +67,7 @@ public ApiResponse createMember(@Valid @RequestBody CreateMemberRequest reque } @PatchMapping("/me") - public ApiResponse editMember(@UserContext MemberDto memberDto, @Valid @RequestBody EditMemberRequest request, + public ApiResponse editMember(@UserContext MemberDto memberDto, @Valid @RequestBody EditMemberRequest request, BindingResult bindingResult) throws PasswordPolicyException, PasswordFailedExceededException { memberService.checkPasswordDoubly(request.getNewPassword(), request.getConfirmPassword()); @@ -76,13 +76,13 @@ public ApiResponse editMember(@UserContext MemberDto memberDto, @Valid @Reque } @DeleteMapping("/me") - public ApiResponse deleteMember(@UserContext MemberDto memberDto) { + public ApiResponse deleteMember(@UserContext MemberDto memberDto) { memberService.deleteByMemberId(memberDto.getMemberId()); return ApiResponse.createSuccessWithNoContent(); } @PostMapping("/signup") - public ApiResponse signUpWithTermAgreement(@UserContext MemberDto memberDto, + public ApiResponse signUpWithTermAgreement(@UserContext MemberDto memberDto, @RequestBody List agreements) { termService.agreeTerms( memberDto.getMemberId(), @@ -94,7 +94,7 @@ public ApiResponse signUpWithTermAgreement(@UserContext MemberDto memberDto, } @DeleteMapping("/signup") - public ApiResponse signUpCancel(@UserContext MemberDto memberDto) { + public ApiResponse signUpCancel(@UserContext MemberDto memberDto) { memberService.deleteByMemberId(memberDto.getMemberId()); return ApiResponse.createSuccessWithNoContent(); } diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java index 1c84384..a2ad848 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/MemberProfileController.java @@ -39,13 +39,13 @@ public class MemberProfileController { private final BudgetService budgetService; @GetMapping("/me") - public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { + public ApiResponse getMemberProfilePageInfo(@UserContext MemberDto memberDto) { MemberProfile profile = memberProfileService.getProfileFetch(memberDto.getProfileId()); return ApiResponse.createSuccess(new MemberProfilePageResponse(profile, memberDto)); } @PostMapping() - public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, + public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, @Validated @RequestBody MemberProfileRequest request) { memberProfileService.createProfile(request.getNickName(), memberDto.getMemberId(), request.getMemberType(), request.getGroupId()); @@ -53,7 +53,7 @@ public ApiResponse createMemberProfile(@UserContext MemberDto memberDto, } @PatchMapping("/me") - public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, + public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, @Validated @RequestBody MemberProfileRequest request) { memberProfileService.changeProfile(memberDto.getProfileId(), request.getNickName(), request.getMemberType(), request.getGroupId()); @@ -61,13 +61,13 @@ public ApiResponse changeMemberProfile(@UserContext MemberDto memberDto, } @PostMapping("/me/addresses/{id}/primary") - public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { + public ApiResponse changePrimaryAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { memberProfileService.changeAddressToPrimary(memberDto.getProfileId(), addressId); return ApiResponse.createSuccessWithNoContent(); } @PostMapping("/me/addresses") - public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressCURequest request) { + public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressCURequest request) { Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); memberProfileService.saveNewAddress(memberDto.getProfileId(), address, request.getAlias(), request.getAddressType()); @@ -75,7 +75,7 @@ public ApiResponse registerAddress(@UserContext MemberDto memberDto, AddressC } @PatchMapping("/me/addresses/{id}") - public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId, + public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId, AddressCURequest request) { Address address = addressApiService.createAddressFromRequest(request.toAddressApiRequest()); memberProfileService.changeAddress(memberDto.getProfileId(), addressId, address, request.getAlias(), @@ -84,13 +84,13 @@ public ApiResponse changeAddress(@UserContext MemberDto memberDto, @PathVaria } @DeleteMapping("/me/addresses/{id}") - public ApiResponse deleteAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { + public ApiResponse deleteAddress(@UserContext MemberDto memberDto, @PathVariable("id") Long addressId) { memberProfileService.deleteAddress(memberDto.getProfileId(), addressId); return ApiResponse.createSuccessWithNoContent(); } @GetMapping("/me/preferences") - public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { + public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { List preferences = memberCategoryPreferenceService.getPreferences(memberDto.getProfileId()); @@ -114,7 +114,7 @@ public ApiResponse getCategoryPreferences(@UserContext MemberDto memberDto) { } @PostMapping("/me/preferences") - public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, + public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, @RequestBody PreferencesRequest request) { memberCategoryPreferenceService.savePreferences( memberDto.getProfileId(), @@ -124,7 +124,7 @@ public ApiResponse saveCategoryPreferences(@UserContext MemberDto memberDto, } @PostMapping("/me/budgets") - public ApiResponse createDefaultBudgets(@UserContext MemberDto memberDto, + public ApiResponse createDefaultBudgets(@UserContext MemberDto memberDto, @RequestBody BudgetRequest budgetRequest) { memberProfileService.registerDefaultBudgets(memberDto.getProfileId(), budgetRequest.getDailyLimit(), budgetRequest.getMonthlyLimit()); diff --git a/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java b/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java index 4f27fda..91bedb8 100644 --- a/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java +++ b/src/main/java/com/stcom/smartmealtable/web/controller/TermController.java @@ -19,7 +19,7 @@ public class TermController { private final TermService termService; @GetMapping() - public ApiResponse getTerms() { + public ApiResponse> getTerms() { List result = termService.findAll(); return ApiResponse.createSuccess(result.stream() .map(TermResponse::new) diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java index 5b3527e..ee70c47 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java +++ b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java @@ -26,12 +26,12 @@ public static ApiResponse createSuccess(T data) { return new ApiResponse<>(SUCCESS_STATUS, null, data); } - public static ApiResponse createSuccessWithNoContent() { + public static ApiResponse createSuccessWithNoContent() { return new ApiResponse<>(SUCCESS_STATUS, null, null); } // Hibernate Validator에 의해 유효하지 않은 데이터로 인해 API 호출이 거부될때 반환 - public static ApiResponse createFail(BindingResult bindingResult) { + public static ApiResponse> createFail(BindingResult bindingResult) { Map errors = new HashMap<>(); List allErrors = bindingResult.getAllErrors(); @@ -46,7 +46,7 @@ public static ApiResponse createFail(BindingResult bindingResult) { } // 예외 발생으로 API 호출 실패시 반환 - public static ApiResponse createError(String message) { + public static ApiResponse createError(String message) { return new ApiResponse<>(ERROR_STATUS, message, null); } From 6bf81a54ca5ab0b257ae887e6181059da163298e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 17:00:07 +0900 Subject: [PATCH 104/120] =?UTF-8?q?test:=20Auth=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/Budget/BudgetTest.java | 28 +-- .../domain/member/MemberTest.java | 95 ++++++++++ .../KakaoAddressApiServiceTest.java | 26 +++ .../integration/MemberIntegrationTest.java | 68 ++++++++ .../repository/MemberRepositoryTest.java | 103 +++++++++++ .../service/MemberServiceTest.java | 157 +++++++++++++++++ .../UserContextArgumentResolverTest.java | 152 ++++++++++++++++ .../web/controller/MemberControllerTest.java | 164 ++++++++++++++++++ 8 files changed, 779 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java index 5b87a4a..1f7ca56 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/BudgetTest.java @@ -2,7 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; import java.math.BigDecimal; import java.time.LocalDate; import java.time.YearMonth; @@ -14,10 +14,10 @@ class BudgetTest { void 예산_생성() throws Exception { // given - Member member = getMember(); + MemberProfile profile = getMember(); // when - DailyBudget budget1 = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); - MonthlyBudget budget2 = new MonthlyBudget(member, BigDecimal.valueOf(100000), YearMonth.now()); + DailyBudget budget1 = new DailyBudget(profile, BigDecimal.valueOf(100000), LocalDate.now()); + MonthlyBudget budget2 = new MonthlyBudget(profile, BigDecimal.valueOf(100000), YearMonth.now()); // then assertThat(budget1.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); assertThat(budget1.getDate()).isNotNull(); @@ -25,15 +25,15 @@ class BudgetTest { assertThat(budget2.getLimit()).isEqualTo(BigDecimal.valueOf(100000)); } - private Member getMember() { - return new Member(); + private MemberProfile getMember() { + return new MemberProfile(); } @Test void 예산_소비_정수() throws Exception { // given - Member member = getMember(); - Budget budget = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); + MemberProfile profile = getMember(); + Budget budget = new DailyBudget(profile, BigDecimal.valueOf(100000), LocalDate.now()); // when budget.addSpent(1000); // then @@ -44,8 +44,8 @@ private Member getMember() { @Test void 예산_소비_소수() throws Exception { // given - Member member = getMember(); - Budget budget = new DailyBudget(member, BigDecimal.valueOf(100000), LocalDate.now()); + MemberProfile profile = getMember(); + Budget budget = new DailyBudget(profile, BigDecimal.valueOf(100000), LocalDate.now()); // when budget.addSpent(9999.9); // then @@ -56,10 +56,10 @@ private Member getMember() { @Test void 예산_초과_유무() throws Exception { // given - Member member1 = getMember(); - Member member2 = getMember(); - Budget budget1 = new DailyBudget(member1, BigDecimal.valueOf(100000), LocalDate.now()); - Budget budget2 = new DailyBudget(member2, BigDecimal.valueOf(100000), LocalDate.now()); + MemberProfile profile1 = getMember(); + MemberProfile profile2 = getMember(); + Budget budget1 = new DailyBudget(profile1, BigDecimal.valueOf(100000), LocalDate.now()); + Budget budget2 = new DailyBudget(profile2, BigDecimal.valueOf(100000), LocalDate.now()); // when budget1.addSpent(99000); diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java new file mode 100644 index 0000000..2721500 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java @@ -0,0 +1,95 @@ +package com.stcom.smartmealtable.domain.member; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MemberTest { + + @Test + @DisplayName("회원 생성 시 이메일, 이름, 비밀번호가 정상적으로 설정되어야 한다") + void createMember() throws PasswordPolicyException { + // given + String email = "test@example.com"; + String fullName = "홍길동"; + String password = "Password123!"; + + // when + Member member = Member.builder() + .email(email) + .fullName(fullName) + .rawPassword(password) + .build(); + + // then + assertThat(member.getEmail()).isEqualTo(email); + assertThat(member.getFullName()).isEqualTo(fullName); + assertThat(member.isEmailVerified()).isTrue(); // 기본값 true + } + + @Test + @DisplayName("유효하지 않은 형식의 비밀번호로 회원을 생성하면 예외가 발생한다") + void createMemberWithInvalidPassword() { + // given + String email = "test@example.com"; + String fullName = "홍길동"; + String weakPassword = "123456"; // 취약한 비밀번호 + + // when & then + assertThatThrownBy(() -> Member.builder() + .email(email) + .fullName(fullName) + .rawPassword(weakPassword) + .build()) + .isInstanceOf(PasswordPolicyException.class); + } + + @Test + @DisplayName("비밀번호 변경이 정상적으로 동작해야 한다") + void changePassword() throws PasswordPolicyException, PasswordFailedExceededException { + // given + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + // when + member.changePassword("Password123!", "NewPassword123!"); + + // then + assertThat(member.isMatchedPassword("NewPassword123!")).isTrue(); + } + + @Test + @DisplayName("잘못된 비밀번호로 비밀번호 변경 시 예외가 발생해야 한다") + void changePasswordWithIncorrectPassword() throws PasswordPolicyException { + // given + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + // when & then + assertThatThrownBy(() -> member.changePassword("WrongPassword123!", "NewPassword123!")) + .isInstanceOf(PasswordFailedExceededException.class); + } + + @Test + @DisplayName("이메일 인증 상태가 정상적으로 변경되어야 한다") + void verifyEmail() throws PasswordPolicyException { + // given + Member member = new Member("test@example.com"); + + // when + member.verifyEmail(); + + // then + assertThat(member.isEmailVerified()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiServiceTest.java b/src/test/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiServiceTest.java new file mode 100644 index 0000000..780c790 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/infrastructure/KakaoAddressApiServiceTest.java @@ -0,0 +1,26 @@ +package com.stcom.smartmealtable.infrastructure; + +import com.stcom.smartmealtable.infrastructure.dto.AddressRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class KakaoAddressApiServiceTest { + + @InjectMocks + private KakaoAddressApiService kakaoAddressApiService; + + @Test + @DisplayName("카카오 주소 API 서비스 테스트") + void kakaoAddressApiServiceTest() { + // given + ReflectionTestUtils.setField(kakaoAddressApiService, "clientId", "test-client-id"); + + // 실제 API 호출이 필요한 테스트는 통합 테스트에서 수행해야 합니다. + // 이 단위 테스트에서는 RestClient 모킹이 복잡하므로 생략합니다. + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java new file mode 100644 index 0000000..6b99bc2 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java @@ -0,0 +1,68 @@ +package com.stcom.smartmealtable.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.repository.MemberRepository; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +class MemberIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("회원 가입 통합 테스트") + void createMember() throws Exception { + // given + String email = "test@example.com"; + String password = "Password123!"; + String fullName = "홍길동"; + + Map request = new HashMap<>(); + request.put("email", email); + request.put("password", password); + request.put("confirmPassword", password); + request.put("fullName", fullName); + + // when & then + mockMvc.perform(post("/api/v1/members") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").isNotEmpty()) + .andExpect(jsonPath("$.data.refreshToken").isNotEmpty()) + .andExpect(jsonPath("$.data.newUser").value(true)); + + // DB 확인 + Member savedMember = memberRepository.findByEmail(email).orElse(null); + assertThat(savedMember).isNotNull(); + assertThat(savedMember.getEmail()).isEqualTo(email); + assertThat(savedMember.getFullName()).isEqualTo(fullName); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java new file mode 100644 index 0000000..149bc27 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java @@ -0,0 +1,103 @@ +package com.stcom.smartmealtable.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +@DataJpaTest +class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TestEntityManager entityManager; + + @Test + @DisplayName("회원을 저장하고 ID로 조회할 수 있어야 한다") + void saveAndFindById() throws PasswordPolicyException { + // given + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + // when + Member savedMember = memberRepository.save(member); + entityManager.flush(); + entityManager.clear(); + + // then + Optional foundMember = memberRepository.findById(savedMember.getId()); + assertThat(foundMember).isPresent(); + assertThat(foundMember.get().getEmail()).isEqualTo("test@example.com"); + assertThat(foundMember.get().getFullName()).isEqualTo("홍길동"); + } + + @Test + @DisplayName("이메일로 회원을 조회할 수 있어야 한다") + void findByEmail() throws PasswordPolicyException { + // given + String email = "test@example.com"; + Member member = Member.builder() + .email(email) + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + memberRepository.save(member); + entityManager.flush(); + entityManager.clear(); + + // when + Optional foundMember = memberRepository.findByEmail(email); + + // then + assertThat(foundMember).isPresent(); + assertThat(foundMember.get().getEmail()).isEqualTo(email); + } + + @Test + @DisplayName("존재하지 않는 이메일로 조회하면 빈 Optional을 반환해야 한다") + void findByEmailNotFound() { + // given + String nonExistentEmail = "nonexistent@example.com"; + + // when + Optional foundMember = memberRepository.findByEmail(nonExistentEmail); + + // then + assertThat(foundMember).isEmpty(); + } + + @Test + @DisplayName("회원을 삭제할 수 있어야 한다") + void deleteMember() throws PasswordPolicyException { + // given + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + Member savedMember = memberRepository.save(member); + entityManager.flush(); + + // when + memberRepository.delete(savedMember); + entityManager.flush(); + entityManager.clear(); + + // then + Optional foundMember = memberRepository.findById(savedMember.getId()); + assertThat(foundMember).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java new file mode 100644 index 0000000..ece2673 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberServiceTest.java @@ -0,0 +1,157 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.repository.AddressEntityRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private MemberProfileRepository memberProfileRepository; + + @Mock + private SocialAccountRepository socialAccountRepository; + + @Mock + private AddressEntityRepository addressEntityRepository; + + @InjectMocks + private MemberService memberService; + + @Test + @DisplayName("회원 정보를 정상적으로 저장할 수 있어야 한다") + void saveMember() throws PasswordPolicyException { + // given + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + when(memberRepository.save(any(Member.class))).thenReturn(member); + + // when + memberService.saveMember(member); + + // then + verify(memberRepository, times(1)).save(any(Member.class)); + } + + @Test + @DisplayName("ID로 회원 조회 시 회원이 존재하면 회원 정보를 반환해야 한다") + void findMemberByMemberId() throws PasswordPolicyException { + // given + Long memberId = 1L; + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + // when + Member foundMember = memberService.findMemberByMemberId(memberId); + + // then + assertThat(foundMember).isEqualTo(member); + verify(memberRepository, times(1)).findById(memberId); + } + + @Test + @DisplayName("ID로 회원 조회 시 회원이 존재하지 않으면 예외가 발생해야 한다") + void findMemberByMemberIdNotFound() { + // given + Long memberId = 999L; + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> memberService.findMemberByMemberId(memberId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("비밀번호 확인이 일치하지 않으면 예외가 발생해야 한다") + void checkPasswordDoublyFail() { + // given + String password = "Password123!"; + String confirmPassword = "DifferentPassword123!"; + + // when & then + assertThatThrownBy(() -> memberService.checkPasswordDoubly(password, confirmPassword)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("비밀번호 변경이 정상적으로 동작해야 한다") + void changePassword() throws PasswordPolicyException, PasswordFailedExceededException { + // given + Long memberId = 1L; + String originPassword = "OriginPassword123!"; + String newPassword = "NewPassword123!"; + + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword(originPassword) + .build(); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + // when + memberService.changePassword(memberId, originPassword, newPassword); + + // then + assertThat(member.isMatchedPassword(newPassword)).isTrue(); + } + + @Test + @DisplayName("회원 삭제가 정상적으로 동작해야 한다") + void deleteByMemberId() throws PasswordPolicyException { + // given + Long memberId = 1L; + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + doNothing().when(memberProfileRepository).deleteMemberProfileByMember(any(Member.class)); + doNothing().when(socialAccountRepository).deleteSocialAccountByMember(any(Member.class)); + doNothing().when(memberRepository).delete(any(Member.class)); + + // when + memberService.deleteByMemberId(memberId); + + // then + verify(memberProfileRepository, times(1)).deleteMemberProfileByMember(member); + verify(socialAccountRepository, times(1)).deleteSocialAccountByMember(member); + verify(memberRepository, times(1)).delete(member); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java b/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java new file mode 100644 index 0000000..5b3a7db --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java @@ -0,0 +1,152 @@ +package com.stcom.smartmealtable.web.argumentresolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; + +@ExtendWith(MockitoExtension.class) +class UserContextArgumentResolverTest { + + @Mock + private JwtTokenService jwtTokenService; + + @Mock + private NativeWebRequest webRequest; + + @Mock + private HttpServletRequest httpServletRequest; + + @Mock + private MethodParameter methodParameter; + + @InjectMocks + private UserContextArgumentResolver resolver; + + @BeforeEach + void setUp() { + when(webRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(httpServletRequest); + } + + @Test + @DisplayName("UserContext 어노테이션과 MemberDto 타입의 파라미터를 지원해야 한다") + void supportsParameter() { + // given + when(methodParameter.hasParameterAnnotation(UserContext.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class) MemberDto.class); + + // when + boolean result = resolver.supportsParameter(methodParameter); + + // then + assertThat(result).isTrue(); + } + + @Test + @DisplayName("UserContext 어노테이션이 없으면 파라미터를 지원하지 않아야 한다") + void doesNotSupportParameterWithoutAnnotation() { + // given + when(methodParameter.hasParameterAnnotation(UserContext.class)).thenReturn(false); + when(methodParameter.getParameterType()).thenReturn((Class) MemberDto.class); + + // when + boolean result = resolver.supportsParameter(methodParameter); + + // then + assertThat(result).isFalse(); + } + + @Test + @DisplayName("MemberDto 타입이 아니면 파라미터를 지원하지 않아야 한다") + void doesNotSupportParameterWithWrongType() { + // given + when(methodParameter.hasParameterAnnotation(UserContext.class)).thenReturn(true); + when(methodParameter.getParameterType()).thenReturn((Class) String.class); + + // when + boolean result = resolver.supportsParameter(methodParameter); + + // then + assertThat(result).isFalse(); + } + + @Test + @DisplayName("유효한 토큰으로부터 MemberDto를 추출해야 한다") + void resolveArgumentWithValidToken() throws Exception { + // given + String token = "Bearer valid-token"; + when(httpServletRequest.getHeader("Authorization")).thenReturn(token); + + Claims claims = mock(Claims.class); + when(claims.get("memberId", String.class)).thenReturn("1"); + when(claims.get("profileId", String.class)).thenReturn("2"); + when(claims.get("email", String.class)).thenReturn("test@example.com"); + when(claims.containsKey("profileId")).thenReturn(true); + when(claims.containsKey("email")).thenReturn(true); + + when(jwtTokenService.extractClaims(anyString())).thenReturn(claims); + + // when + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + // then + assertThat(result).isInstanceOf(MemberDto.class); + MemberDto memberDto = (MemberDto) result; + assertThat(memberDto.getMemberId()).isEqualTo(1L); + assertThat(memberDto.getProfileId()).isEqualTo(2L); + assertThat(memberDto.getEmail()).isEqualTo("test@example.com"); + } + + @Test + @DisplayName("토큰이 없으면 예외가 발생해야 한다") + void resolveArgumentWithNoToken() { + // given + when(httpServletRequest.getHeader("Authorization")).thenReturn(null); + + // when & then + assertThatThrownBy(() -> resolver.resolveArgument(methodParameter, null, webRequest, null)) + .isInstanceOf(RuntimeException.class) + .hasMessage("권한 없음."); + } + + @Test + @DisplayName("프로필 ID가 없는 토큰으로부터 MemberDto를 추출할 수 있어야 한다") + void resolveArgumentWithTokenWithoutProfileId() throws Exception { + // given + String token = "Bearer valid-token"; + when(httpServletRequest.getHeader("Authorization")).thenReturn(token); + + Claims claims = mock(Claims.class); + when(claims.get("memberId", String.class)).thenReturn("1"); + when(claims.get("email", String.class)).thenReturn("test@example.com"); + when(claims.containsKey("profileId")).thenReturn(false); + when(claims.containsKey("email")).thenReturn(true); + + when(jwtTokenService.extractClaims(anyString())).thenReturn(claims); + + // when + Object result = resolver.resolveArgument(methodParameter, null, webRequest, null); + + // then + assertThat(result).isInstanceOf(MemberDto.class); + MemberDto memberDto = (MemberDto) result; + assertThat(memberDto.getMemberId()).isEqualTo(1L); + assertThat(memberDto.getProfileId()).isNull(); + assertThat(memberDto.getEmail()).isEqualTo("test@example.com"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java b/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java new file mode 100644 index 0000000..8ef5ac9 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java @@ -0,0 +1,164 @@ +package com.stcom.smartmealtable.web.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.infrastructure.dto.JwtTokenResponseDto; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.MemberService; +import com.stcom.smartmealtable.service.TermService; +import com.stcom.smartmealtable.service.dto.MemberDto; +import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.controller.MemberController.CreateMemberRequest; +import com.stcom.smartmealtable.web.controller.MemberController.EditMemberRequest; +import com.stcom.smartmealtable.web.controller.MemberController.TermAgreementDto; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.RestController; + +@WebMvcTest(value = MemberController.class, + includeFilters = { + @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {RestController.class}) + }) +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MemberService memberService; + + @MockBean + private JwtTokenService jwtTokenService; + + @MockBean + private TermService termService; + + @Test + @DisplayName("이메일 중복 확인 API가 정상적으로 동작해야 한다") + void checkEmail() throws Exception { + // given + String email = "test@example.com"; + doNothing().when(memberService).validateDuplicatedEmail(email); + + // when & then + mockMvc.perform(get("/api/v1/members/email/check") + .param("email", email)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("회원 생성 API가 정상적으로 동작해야 한다") + void createMember() throws Exception { + // given + CreateMemberRequest request = new CreateMemberRequest( + "test@example.com", "Password123!", "Password123!", "홍길동"); + + JwtTokenResponseDto tokenDto = new JwtTokenResponseDto( + "test-access-token", + "test-refresh-token", + 3600, + "Bearer" + ); + tokenDto.setNewUser(true); + + when(jwtTokenService.createTokenDto(anyLong(), any())).thenReturn(tokenDto); + doNothing().when(memberService).validateDuplicatedEmail(anyString()); + doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); + + // when & then + mockMvc.perform(post("/api/v1/members") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.accessToken").value("test-access-token")) + .andExpect(jsonPath("$.data.refreshToken").value("test-refresh-token")) + .andExpect(jsonPath("$.data.newUser").value(true)); + } + + @Test + @DisplayName("회원 정보 수정 API가 정상적으로 동작해야 한다") + void editMember() throws Exception { + // given + EditMemberRequest request = new EditMemberRequest( + "OldPassword123!", "NewPassword123!", "NewPassword123!"); + + MemberDto memberDto = MemberDto.builder() + .memberId(1L) + .build(); + + // when & then + mockMvc.perform(patch("/api/v1/members/me") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("회원 탈퇴 API가 정상적으로 동작해야 한다") + void deleteMember() throws Exception { + // given + MemberDto memberDto = MemberDto.builder() + .memberId(1L) + .build(); + + doNothing().when(memberService).deleteByMemberId(anyLong()); + + // when & then + mockMvc.perform(delete("/api/v1/members/me") + .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } + + @Test + @DisplayName("약관 동의 API가 정상적으로 동작해야 한다") + void signUpWithTermAgreement() throws Exception { + // given + List agreements = Arrays.asList( + new TermAgreementDto(1L, true), + new TermAgreementDto(2L, false) + ); + + MemberDto memberDto = MemberDto.builder() + .memberId(1L) + .build(); + + doNothing().when(termService).agreeTerms(anyLong(), any()); + + // when & then + mockMvc.perform(post("/api/v1/members/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(agreements)) + .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + } +} \ No newline at end of file From a4f382ceeef06c34efaae0c9c344be9c25751410 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 17:00:19 +0900 Subject: [PATCH 105/120] =?UTF-8?q?chore:=20CORS=20Origins=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/stcom/smartmealtable/web/WebConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java index 25d2099..0be154d 100644 --- a/src/main/java/com/stcom/smartmealtable/web/WebConfig.java +++ b/src/main/java/com/stcom/smartmealtable/web/WebConfig.java @@ -31,8 +31,8 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000") - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedOrigins("http://localhost:3000", "http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } From 0c4d19e38a8fbfce31e862d900e2aad6e57d54a9 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 17:28:27 +0900 Subject: [PATCH 106/120] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=20Contro?= =?UTF-8?q?llderAdvice=EB=A1=9C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/exhandler/ExControllerAdvice.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java diff --git a/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java new file mode 100644 index 0000000..77d517d --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java @@ -0,0 +1,65 @@ +package com.stcom.smartmealtable.web.exhandler; + +import com.stcom.smartmealtable.exception.ExternApiStatusError; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.web.dto.ApiResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ExControllerAdvice { + + @ExceptionHandler(PasswordPolicyException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse passwordPolicyExHandler(PasswordPolicyException e) { + log.error("[PasswordPolicyException] ex", e); + return ApiResponse.createError(e.getMessage()); + } + + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse illegalArgumentExHandler(IllegalArgumentException e) { + log.error("[IllegalArgumentException] ex", e); + return ApiResponse.createError("파라미터나 API 스펙을 확인해보세요" + e.getMessage()); + } + + @ExceptionHandler(IllegalStateException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse illegalStateExHandler(IllegalArgumentException e) { + log.error("[IllegalStateException] ex", e); + return ApiResponse.createError("파라미터나 API 스펙을 확인해보세요" + e.getMessage()); + } + + @ExceptionHandler(PasswordFailedExceededException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiResponse passwordFailedExceededExHandler(PasswordFailedExceededException e) { + log.error("[PasswordFailedExceededException] ex", e); + return ApiResponse.createError(e.getMessage()); + } + + @ExceptionHandler(ExternApiStatusError.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse externApiStatusErrorHandler(ExternApiStatusError e) { + log.error("[ExternApiStatusError] ex", e); + return ApiResponse.createError("외부 API 호출 중 오류가 발생했습니다: " + e.getMessage()); + } + + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse runtimeExHandler(Exception e) { + log.error("[RuntimeException] ex", e); + return ApiResponse.createError("서버 내부에서 언체크 예외가 발생했습니다"); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse exHandler(Exception e) { + log.error("[Exception] ex", e); + return ApiResponse.createError("서버 내부에서 체크 예외가 발생했습니다"); + } +} From 67c3c3b6236ffa054f5bbd6ba6e60bf6fc8541c7 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 18:05:32 +0900 Subject: [PATCH 107/120] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../web/exhandler/ExControllerAdvice.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fe17620..5663886 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화/역직렬화에 Jackson 사용 compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java index 77d517d..0e1fd83 100644 --- a/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java +++ b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java @@ -6,14 +6,25 @@ import com.stcom.smartmealtable.web.dto.ApiResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; @Slf4j @RestControllerAdvice public class ExControllerAdvice { + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse processValidationError(MethodArgumentNotValidException exception) { + BindingResult bindingResult = exception.getBindingResult(); + return ApiResponse.createFail(bindingResult); + } + + @ExceptionHandler(PasswordPolicyException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ApiResponse passwordPolicyExHandler(PasswordPolicyException e) { @@ -49,6 +60,14 @@ public ApiResponse externApiStatusErrorHandler(ExternApiStatusError e) { return ApiResponse.createError("외부 API 호출 중 오류가 발생했습니다: " + e.getMessage()); } + @ExceptionHandler(HttpClientErrorException.BadRequest.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleHttpClientErrorBadRequest(HttpClientErrorException.BadRequest e) { + log.error("[HttpClientErrorException.BadRequest] ex", e); + String responseBody = e.getResponseBodyAsString(); + return ApiResponse.createError("외부 OAuth 인증 실패: 잘못된 인증 코드입니다. " + responseBody); + } + @ExceptionHandler(RuntimeException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse runtimeExHandler(Exception e) { From d612e7ea13f4e4bd7a6575eff4e612624a3dfbb3 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Thu, 22 May 2025 18:15:10 +0900 Subject: [PATCH 108/120] =?UTF-8?q?feat:=20HandlerMethodValidation=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/web/exhandler/ExControllerAdvice.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java index 0e1fd83..8764a4e 100644 --- a/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java +++ b/src/main/java/com/stcom/smartmealtable/web/exhandler/ExControllerAdvice.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.method.annotation.HandlerMethodValidationException; @Slf4j @RestControllerAdvice @@ -24,6 +25,12 @@ public ApiResponse processValidationError(MethodArgumentNotValidException exc return ApiResponse.createFail(bindingResult); } + @ExceptionHandler(HandlerMethodValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse processValidationValidatorError(HandlerMethodValidationException exception) { + return ApiResponse.createError(exception.getMessage()); + } + @ExceptionHandler(PasswordPolicyException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) From 749db89626ca45b7f6a3dff4eac06155376b513e Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:52:16 +0900 Subject: [PATCH 109/120] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20DB=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 5663886..5e1b84f 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.batch:spring-batch-test' + testImplementation 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } From 10d452a5d9d989e6f2d82c78de948938bccd2762 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:52:35 +0900 Subject: [PATCH 110/120] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stcom/smartmealtable/domain/member/Member.java | 2 +- .../stcom/smartmealtable/domain/member/MemberPassword.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java index 175c703..ef3cb8b 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/Member.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/Member.java @@ -60,7 +60,7 @@ public void changePassword(String rawOldPassword, String rawNewPassword) if (rawOldPassword.isBlank() || rawNewPassword.isBlank()) { throw new IllegalArgumentException("빈 비밀번호를 입력했습니다"); } - password.changePassword(rawOldPassword, rawNewPassword); + password.changePassword(rawNewPassword, rawOldPassword); } public boolean isMatchedPassword(final String rawPassword) throws PasswordFailedExceededException { diff --git a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java index c60f9a3..9e35bb9 100644 --- a/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java +++ b/src/main/java/com/stcom/smartmealtable/domain/member/MemberPassword.java @@ -46,8 +46,8 @@ public void checkPasswordPolicy(String rawPassword) throws PasswordPolicyExcepti throw new PasswordPolicyException("비밀번호는 최대 20자까지 가능합니다."); } - if (!rawPassword.matches("^[A-Za-z0-9]+$")) { - throw new PasswordPolicyException("비밀번호는 영문자(A–Z, a–z)와 숫자(0–9)로만 구성되어야 합니다."); + if (!rawPassword.matches("^[\\x21-\\x7E]+$")) { + throw new PasswordPolicyException("비밀번호는 영문자, 숫자, 특수문자로만 구성되어야 합니다."); } } From 1ee9df273528fab45560c22bae10e1f8ab2e1d7d Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:52:52 +0900 Subject: [PATCH 111/120] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stcom/smartmealtable/service/MemberService.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/service/MemberService.java b/src/main/java/com/stcom/smartmealtable/service/MemberService.java index c7a738f..a358e1c 100644 --- a/src/main/java/com/stcom/smartmealtable/service/MemberService.java +++ b/src/main/java/com/stcom/smartmealtable/service/MemberService.java @@ -21,8 +21,17 @@ public class MemberService { private final SocialAccountRepository socialAccountRepository; private final AddressEntityRepository addressEntityRepository; + /** + * 이메일 중복 검사를 수행합니다. + * 동일한 이메일을 가진 회원이 이미 존재하면 예외를 발생시킵니다. + * + * @param email 중복 검사할 이메일 + * @throws IllegalArgumentException 이메일이 이미 존재하는 경우 + */ public void validateDuplicatedEmail(String email) { - memberRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException("이미 존재하는 이메일 입니다")); + memberRepository.findByEmail(email).ifPresent(member -> { + throw new IllegalArgumentException("이미 존재하는 이메일 입니다"); + }); } @Transactional From 1d4e0dbc9cf0df7a604025848fa4e346ada425dc Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:53:08 +0900 Subject: [PATCH 112/120] =?UTF-8?q?refactor:=20Group=20Service=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/service/GroupService.java | 23 ++------------- .../service/GroupServiceImpl.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/stcom/smartmealtable/service/GroupServiceImpl.java diff --git a/src/main/java/com/stcom/smartmealtable/service/GroupService.java b/src/main/java/com/stcom/smartmealtable/service/GroupService.java index 394fcc0..055cd74 100644 --- a/src/main/java/com/stcom/smartmealtable/service/GroupService.java +++ b/src/main/java/com/stcom/smartmealtable/service/GroupService.java @@ -1,26 +1,9 @@ package com.stcom.smartmealtable.service; import com.stcom.smartmealtable.domain.group.Group; -import com.stcom.smartmealtable.repository.GroupRepository; import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Limit; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class GroupService { - - private final GroupRepository groupRepository; - - public Group findGroupByGroupId(Long groupId) { - return groupRepository.findById(groupId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); - } - - public List findGroupsByKeyword(String keyword) { - return groupRepository.findByNameContaining(keyword, Limit.of(10)); - } +public interface GroupService { + Group findGroupByGroupId(Long groupId); + List findGroupsByKeyword(String keyword); } diff --git a/src/main/java/com/stcom/smartmealtable/service/GroupServiceImpl.java b/src/main/java/com/stcom/smartmealtable/service/GroupServiceImpl.java new file mode 100644 index 0000000..9fccfd8 --- /dev/null +++ b/src/main/java/com/stcom/smartmealtable/service/GroupServiceImpl.java @@ -0,0 +1,28 @@ +package com.stcom.smartmealtable.service; + +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.repository.GroupRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Limit; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GroupServiceImpl implements GroupService { + + private final GroupRepository groupRepository; + + @Override + public Group findGroupByGroupId(Long groupId) { + return groupRepository.findById(groupId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다")); + } + + @Override + public List findGroupsByKeyword(String keyword) { + return groupRepository.findByNameContaining(keyword, Limit.of(10)); + } +} \ No newline at end of file From d83eed1e3a36374ed516851c0fa4452231f4f67f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:53:23 +0900 Subject: [PATCH 113/120] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B0=9D=EC=B2=B4=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=BC=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java index ee70c47..a2bf618 100644 --- a/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java +++ b/src/main/java/com/stcom/smartmealtable/web/dto/ApiResponse.java @@ -26,7 +26,7 @@ public static ApiResponse createSuccess(T data) { return new ApiResponse<>(SUCCESS_STATUS, null, data); } - public static ApiResponse createSuccessWithNoContent() { + public static ApiResponse createSuccessWithNoContent() { return new ApiResponse<>(SUCCESS_STATUS, null, null); } From 57a44fceb193dbe697f1c2a0c7ef3ff6738d2bf6 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:53:40 +0900 Subject: [PATCH 114/120] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20Config=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../smartmealtable/config/TestWebConfig.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/config/TestWebConfig.java diff --git a/src/test/java/com/stcom/smartmealtable/config/TestWebConfig.java b/src/test/java/com/stcom/smartmealtable/config/TestWebConfig.java new file mode 100644 index 0000000..597b753 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/config/TestWebConfig.java @@ -0,0 +1,17 @@ +package com.stcom.smartmealtable.config; + +import com.stcom.smartmealtable.security.JwtTokenService; +import org.mockito.Mockito; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +@TestConfiguration +public class TestWebConfig { + + @Bean + @Primary + public JwtTokenService jwtTokenService() { + return Mockito.mock(JwtTokenService.class); + } +} \ No newline at end of file From 5f78f946d832b3e51adf3e0527dbe0764f43d4be Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:54:01 +0900 Subject: [PATCH 115/120] =?UTF-8?q?test:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/Address/AddressTest.java | 74 ++++++++++ .../domain/Budget/DailyBudgetTest.java | 89 ++++++++++++ .../domain/Budget/MonthlyBudgetTest.java | 104 ++++++++++++++ .../domain/food/FoodCategoryTest.java | 55 +++++++ .../food/MemberCategoryPreferenceTest.java | 111 ++++++++++++++ .../domain/group/GroupTest.java | 61 ++++++++ .../domain/member/MemberPasswordTest.java | 10 ++ .../domain/member/MemberTest.java | 15 ++ .../domain/social/SocialAccountTest.java | 136 ++++++++++++++++++ .../smartmealtable/domain/term/TermTest.java | 105 ++++++++++++++ 10 files changed, 760 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/domain/Address/AddressTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/Budget/DailyBudgetTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudgetTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/food/FoodCategoryTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreferenceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/group/GroupTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/social/SocialAccountTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/domain/term/TermTest.java diff --git a/src/test/java/com/stcom/smartmealtable/domain/Address/AddressTest.java b/src/test/java/com/stcom/smartmealtable/domain/Address/AddressTest.java new file mode 100644 index 0000000..822ca03 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/Address/AddressTest.java @@ -0,0 +1,74 @@ +package com.stcom.smartmealtable.domain.Address; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AddressTest { + + @Test + @DisplayName("Address 빌더를 통해 객체가 올바르게 생성된다") + void addressIsCreatedCorrectlyUsingBuilder() { + // given & when + Address address = Address.builder() + .lotNumberAddress("서울특별시 강남구 역삼동 123-45") + .roadAddress("서울특별시 강남구 테헤란로 123") + .detailAddress("4층 401호") + .latitude(37.5012) + .longitude(127.0396) + .build(); + + // then + assertThat(address.getLotNumberAddress()).isEqualTo("서울특별시 강남구 역삼동 123-45"); + assertThat(address.getRoadAddress()).isEqualTo("서울특별시 강남구 테헤란로 123"); + assertThat(address.getDetailAddress()).isEqualTo("4층 401호"); + assertThat(address.getLatitude()).isEqualTo(37.5012); + assertThat(address.getLongitude()).isEqualTo(127.0396); + } + + @Test + @DisplayName("updateAddress 메소드로 주소 정보를 업데이트할 수 있다") + void updateAddressMethodUpdatesAddressInformation() { + // given + Address address = Address.builder() + .lotNumberAddress("서울특별시 강남구 역삼동 123-45") + .roadAddress("서울특별시 강남구 테헤란로 123") + .detailAddress("4층 401호") + .latitude(37.5012) + .longitude(127.0396) + .build(); + + // when + address.updateAddress( + "서울특별시 서초구 서초동 987-65", + "서울특별시 서초구 서초대로 456", + "8층 802호", + 37.4923, + 127.0292 + ); + + // then + assertThat(address.getLotNumberAddress()).isEqualTo("서울특별시 서초구 서초동 987-65"); + assertThat(address.getRoadAddress()).isEqualTo("서울특별시 서초구 서초대로 456"); + assertThat(address.getDetailAddress()).isEqualTo("8층 802호"); + assertThat(address.getLatitude()).isEqualTo(37.4923); + assertThat(address.getLongitude()).isEqualTo(127.0292); + } + + @Test + @DisplayName("AddressType enum 값이 올바르게 정의되어 있다") + void addressTypeEnumHasCorrectValues() { + // given + AddressType[] addressTypes = AddressType.values(); + + // when & then + assertThat(addressTypes).hasSize(4); + assertThat(addressTypes).contains( + AddressType.HOME, + AddressType.SCHOOL, + AddressType.OFFICE, + AddressType.ETC + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/DailyBudgetTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/DailyBudgetTest.java new file mode 100644 index 0000000..f33dcce --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/DailyBudgetTest.java @@ -0,0 +1,89 @@ +package com.stcom.smartmealtable.domain.Budget; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.MemberProfile; +import java.math.BigDecimal; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DailyBudgetTest { + + @Test + @DisplayName("DailyBudget 객체가 정상적으로 생성된다") + void createDailyBudget() { + // given + MemberProfile memberProfile = new MemberProfile(); + BigDecimal limit = BigDecimal.valueOf(10000); + LocalDate today = LocalDate.now(); + + // when + DailyBudget dailyBudget = new DailyBudget(memberProfile, limit, today); + + // then + assertThat(dailyBudget.getMemberProfile()).isEqualTo(memberProfile); + assertThat(dailyBudget.getLimit()).isEqualTo(limit); + assertThat(dailyBudget.getDate()).isEqualTo(today); + assertThat(dailyBudget.getSpendAmount()).isEqualTo(BigDecimal.ZERO); + } + + @Test + @DisplayName("DailyBudget에 소비 금액을 추가할 수 있다") + void addSpentAmount() { + // given + MemberProfile memberProfile = new MemberProfile(); + BigDecimal limit = BigDecimal.valueOf(10000); + LocalDate today = LocalDate.now(); + DailyBudget dailyBudget = new DailyBudget(memberProfile, limit, today); + + // when + dailyBudget.addSpent(BigDecimal.valueOf(3000)); + dailyBudget.addSpent(1000); + dailyBudget.addSpent(1500.5); + + // then + assertThat(dailyBudget.getSpendAmount()).isEqualTo(BigDecimal.valueOf(5500.5)); + assertThat(dailyBudget.getAvailableAmount()).isEqualTo(BigDecimal.valueOf(4499.5)); + } + + @Test + @DisplayName("DailyBudget이 한도를 초과했는지 확인할 수 있다") + void checkIfOverLimit() { + // given + MemberProfile memberProfile = new MemberProfile(); + BigDecimal limit = BigDecimal.valueOf(5000); + LocalDate today = LocalDate.now(); + DailyBudget dailyBudget = new DailyBudget(memberProfile, limit, today); + + // when + dailyBudget.addSpent(3000); + boolean beforeOverLimit = dailyBudget.isOverLimit(); + + dailyBudget.addSpent(2500); + boolean afterOverLimit = dailyBudget.isOverLimit(); + + // then + assertThat(beforeOverLimit).isFalse(); + assertThat(afterOverLimit).isTrue(); + assertThat(dailyBudget.getAvailableAmount()).isEqualTo(BigDecimal.valueOf(-500)); + } + + @Test + @DisplayName("DailyBudget의 소비 금액을 초기화할 수 있다") + void resetSpentAmount() { + // given + MemberProfile memberProfile = new MemberProfile(); + BigDecimal limit = BigDecimal.valueOf(10000); + LocalDate today = LocalDate.now(); + DailyBudget dailyBudget = new DailyBudget(memberProfile, limit, today); + dailyBudget.addSpent(5000); + + // when + dailyBudget.resetSpent(); + + // then + assertThat(dailyBudget.getSpendAmount()).isEqualTo(BigDecimal.ZERO); + assertThat(dailyBudget.getAvailableAmount()).isEqualTo(limit); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudgetTest.java b/src/test/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudgetTest.java new file mode 100644 index 0000000..8bff524 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/Budget/MonthlyBudgetTest.java @@ -0,0 +1,104 @@ +package com.stcom.smartmealtable.domain.Budget; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import com.stcom.smartmealtable.domain.member.MemberProfile; +import java.math.BigDecimal; +import java.time.YearMonth; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MonthlyBudgetTest { + + @Test + @DisplayName("월별 예산을 생성할 수 있다") + void createMonthlyBudget() { + // given + MemberProfile memberProfile = mock(MemberProfile.class); + BigDecimal limit = BigDecimal.valueOf(300000); + YearMonth yearMonth = YearMonth.of(2025, 5); + + // when + MonthlyBudget budget = new MonthlyBudget(memberProfile, limit, yearMonth); + + // then + assertThat(budget.getMemberProfile()).isEqualTo(memberProfile); + assertThat(budget.getLimit()).isEqualTo(limit); + assertThat(budget.getYearMonth()).isEqualTo(yearMonth); + assertThat(budget.getSpendAmount()).isEqualTo(BigDecimal.ZERO); + } + + @Test + @DisplayName("지출 금액을 추가할 수 있다") + void addSpent() { + // given + MemberProfile memberProfile = mock(MemberProfile.class); + BigDecimal limit = BigDecimal.valueOf(300000); + YearMonth yearMonth = YearMonth.of(2025, 5); + MonthlyBudget budget = new MonthlyBudget(memberProfile, limit, yearMonth); + + // when + budget.addSpent(BigDecimal.valueOf(50000)); + budget.addSpent(10000); // int 값 추가 + budget.addSpent(5000.5); // double 값 추가 + + // then + assertThat(budget.getSpendAmount()).isEqualTo(BigDecimal.valueOf(65000.5)); + } + + @Test + @DisplayName("사용 가능한 예산 금액을 계산할 수 있다") + void getAvailableAmount() { + // given + MemberProfile memberProfile = mock(MemberProfile.class); + BigDecimal limit = BigDecimal.valueOf(300000); + YearMonth yearMonth = YearMonth.of(2025, 5); + MonthlyBudget budget = new MonthlyBudget(memberProfile, limit, yearMonth); + + // when + budget.addSpent(BigDecimal.valueOf(100000)); + + // then + assertThat(budget.getAvailableAmount()).isEqualTo(BigDecimal.valueOf(200000)); + } + + @Test + @DisplayName("예산 초과 여부를 확인할 수 있다") + void isOverLimit() { + // given + MemberProfile memberProfile = mock(MemberProfile.class); + BigDecimal limit = BigDecimal.valueOf(300000); + YearMonth yearMonth = YearMonth.of(2025, 5); + MonthlyBudget budget = new MonthlyBudget(memberProfile, limit, yearMonth); + + // when - 예산 이내 지출 + budget.addSpent(BigDecimal.valueOf(200000)); + + // then + assertThat(budget.isOverLimit()).isFalse(); + + // when - 예산 초과 지출 + budget.addSpent(BigDecimal.valueOf(150000)); + + // then + assertThat(budget.isOverLimit()).isTrue(); + } + + @Test + @DisplayName("지출 금액을 초기화할 수 있다") + void resetSpent() { + // given + MemberProfile memberProfile = mock(MemberProfile.class); + BigDecimal limit = BigDecimal.valueOf(300000); + YearMonth yearMonth = YearMonth.of(2025, 5); + MonthlyBudget budget = new MonthlyBudget(memberProfile, limit, yearMonth); + budget.addSpent(BigDecimal.valueOf(100000)); + + // when + budget.resetSpent(); + + // then + assertThat(budget.getSpendAmount()).isEqualTo(BigDecimal.ZERO); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/food/FoodCategoryTest.java b/src/test/java/com/stcom/smartmealtable/domain/food/FoodCategoryTest.java new file mode 100644 index 0000000..3e58394 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/food/FoodCategoryTest.java @@ -0,0 +1,55 @@ +package com.stcom.smartmealtable.domain.food; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class FoodCategoryTest { + + @Test + @DisplayName("FoodCategory 객체의 필드값이 올바르게 설정된다") + void foodCategoryFieldsAreSetCorrectly() { + // given + FoodCategory foodCategory = new FoodCategory(); + + // when + ReflectionTestUtils.setField(foodCategory, "id", 1L); + ReflectionTestUtils.setField(foodCategory, "name", "한식"); + + // then + assertThat(foodCategory.getId()).isEqualTo(1L); + assertThat(foodCategory.getName()).isEqualTo("한식"); + } + + @Test + @DisplayName("다양한 음식 카테고리를 생성하고 구분할 수 있다") + void createAndDistinguishMultipleFoodCategories() { + // given + FoodCategory koreanFood = new FoodCategory(); + FoodCategory chineseFood = new FoodCategory(); + FoodCategory japaneseFood = new FoodCategory(); + FoodCategory westernFood = new FoodCategory(); + + // when + ReflectionTestUtils.setField(koreanFood, "id", 1L); + ReflectionTestUtils.setField(koreanFood, "name", "한식"); + + ReflectionTestUtils.setField(chineseFood, "id", 2L); + ReflectionTestUtils.setField(chineseFood, "name", "중식"); + + ReflectionTestUtils.setField(japaneseFood, "id", 3L); + ReflectionTestUtils.setField(japaneseFood, "name", "일식"); + + ReflectionTestUtils.setField(westernFood, "id", 4L); + ReflectionTestUtils.setField(westernFood, "name", "양식"); + + // then + assertThat(koreanFood.getId()).isNotEqualTo(chineseFood.getId()); + assertThat(koreanFood.getName()).isEqualTo("한식"); + assertThat(chineseFood.getName()).isEqualTo("중식"); + assertThat(japaneseFood.getName()).isEqualTo("일식"); + assertThat(westernFood.getName()).isEqualTo("양식"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreferenceTest.java b/src/test/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreferenceTest.java new file mode 100644 index 0000000..b340caa --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/food/MemberCategoryPreferenceTest.java @@ -0,0 +1,111 @@ +package com.stcom.smartmealtable.domain.food; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.MemberProfile; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class MemberCategoryPreferenceTest { + + @Test + @DisplayName("MemberCategoryPreference 객체가 빌더를 통해 올바르게 생성된다") + void createMemberCategoryPreferenceWithBuilder() { + // given + MemberProfile memberProfile = new MemberProfile(); + FoodCategory foodCategory = new FoodCategory(); + ReflectionTestUtils.setField(foodCategory, "id", 1L); + ReflectionTestUtils.setField(foodCategory, "name", "한식"); + + // when + MemberCategoryPreference preference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(foodCategory) + .type(PreferenceType.LIKE) + .priority(1) + .build(); + + // then + assertThat(preference.getMemberProfile()).isEqualTo(memberProfile); + assertThat(preference.getCategory()).isEqualTo(foodCategory); + assertThat(preference.getCategory().getName()).isEqualTo("한식"); + assertThat(preference.getType()).isEqualTo(PreferenceType.LIKE); + assertThat(preference.getPriority()).isEqualTo(1); + } + + @Test + @DisplayName("선호도와 비선호도를 표현할 수 있다") + void expressLikeAndDislikePreferences() { + // given + MemberProfile memberProfile = new MemberProfile(); + FoodCategory koreanFood = new FoodCategory(); + FoodCategory japaneseFood = new FoodCategory(); + + ReflectionTestUtils.setField(koreanFood, "name", "한식"); + ReflectionTestUtils.setField(japaneseFood, "name", "일식"); + + // when + MemberCategoryPreference likePreference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(koreanFood) + .type(PreferenceType.LIKE) + .priority(1) + .build(); + + MemberCategoryPreference dislikePreference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(japaneseFood) + .type(PreferenceType.DISLIKE) + .priority(1) + .build(); + + // then + assertThat(likePreference.getType()).isEqualTo(PreferenceType.LIKE); + assertThat(likePreference.getCategory().getName()).isEqualTo("한식"); + + assertThat(dislikePreference.getType()).isEqualTo(PreferenceType.DISLIKE); + assertThat(dislikePreference.getCategory().getName()).isEqualTo("일식"); + } + + @Test + @DisplayName("선호도에 우선순위를 부여할 수 있다") + void assignPriorityToPreferences() { + // given + MemberProfile memberProfile = new MemberProfile(); + FoodCategory koreanFood = new FoodCategory(); + FoodCategory chineseFood = new FoodCategory(); + FoodCategory westernFood = new FoodCategory(); + + // when + MemberCategoryPreference firstPreference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(koreanFood) + .type(PreferenceType.LIKE) + .priority(1) + .build(); + + MemberCategoryPreference secondPreference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(chineseFood) + .type(PreferenceType.LIKE) + .priority(2) + .build(); + + MemberCategoryPreference thirdPreference = MemberCategoryPreference.builder() + .memberProfile(memberProfile) + .category(westernFood) + .type(PreferenceType.LIKE) + .priority(3) + .build(); + + // then + assertThat(firstPreference.getPriority()).isLessThan(secondPreference.getPriority()); + assertThat(secondPreference.getPriority()).isLessThan(thirdPreference.getPriority()); + assertThat(firstPreference.getPriority()).isEqualTo(1); + assertThat(secondPreference.getPriority()).isEqualTo(2); + assertThat(thirdPreference.getPriority()).isEqualTo(3); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/group/GroupTest.java b/src/test/java/com/stcom/smartmealtable/domain/group/GroupTest.java new file mode 100644 index 0000000..30349e9 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/group/GroupTest.java @@ -0,0 +1,61 @@ +package com.stcom.smartmealtable.domain.group; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.Address.Address; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class GroupTest { + + @Test + @DisplayName("CompanyGroup의 getTypeName은 industryType의 description을 반환한다") + void companyGroupGetTypeName() { + // given + CompanyGroup companyGroup = new CompanyGroup(); + ReflectionTestUtils.setField(companyGroup, "industryType", IndustryType.IT); + + // when + String typeName = companyGroup.getTypeName(); + + // then + assertThat(typeName).isEqualTo("IT"); + } + + @Test + @DisplayName("SchoolGroup의 getTypeName은 schoolType의 name을 반환한다") + void schoolGroupGetTypeName() { + // given + SchoolGroup schoolGroup = new SchoolGroup(); + ReflectionTestUtils.setField(schoolGroup, "schoolType", SchoolType.UNIVERSITY_FOUR_YEAR); + + // when + String typeName = schoolGroup.getTypeName(); + + // then + assertThat(typeName).isEqualTo("UNIVERSITY_FOUR_YEAR"); + } + + @Test + @DisplayName("IndustryType enum의 getDescription 메소드가 올바른 값을 반환한다") + void industryTypeGetDescription() { + // given & when & then + assertThat(IndustryType.IT.getDescription()).isEqualTo("IT"); + assertThat(IndustryType.FINANCE.getDescription()).isEqualTo("파이낸스"); + assertThat(IndustryType.MANUFACTURING.getDescription()).isEqualTo("제조업"); + assertThat(IndustryType.SERVICE.getDescription()).isEqualTo("서비스"); + } + + @Test + @DisplayName("SchoolType enum의 getDescription 메소드가 올바른 값을 반환한다") + void schoolTypeGetDescription() { + // given & when & then + assertThat(SchoolType.UNIVERSITY_FOUR_YEAR.getDescription()).isEqualTo("대학교(4년제)"); + assertThat(SchoolType.UNIVERSITY_TWO_YEAR.getDescription()).isEqualTo("대학교(2년제)"); + assertThat(SchoolType.HIGH_SCHOOL.getDescription()).isEqualTo("고등학교"); + assertThat(SchoolType.MIDDLE_SCHOOL.getDescription()).isEqualTo("중학교"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java index fef9d7a..7725cb8 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberPasswordTest.java @@ -8,6 +8,7 @@ import com.stcom.smartmealtable.exception.PasswordFailedExceededException; import com.stcom.smartmealtable.exception.PasswordPolicyException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; class MemberPasswordTest { @@ -97,5 +98,14 @@ private void checkFailedCase(String failedRawPassword) { assertTrue(password.isMatched("aaabcd1234")); } + @Test + @DisplayName("비밀번호 연속 실패 5회까지는 false 반환하고, 6회 시 예외 발생해야 한다") + void 비밀번호_연속_실패_제한_초과() throws Exception { + MemberPassword password = createPassword("abcdefg1234"); + for (int i = 0; i < 5; i++) { + assertThat(password.isMatched("wrong")).isFalse(); + } + assertThrows(PasswordFailedExceededException.class, () -> password.isMatched("wrong")); + } } \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java b/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java index 2721500..44c24a8 100644 --- a/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java +++ b/src/test/java/com/stcom/smartmealtable/domain/member/MemberTest.java @@ -92,4 +92,19 @@ void verifyEmail() throws PasswordPolicyException { // then assertThat(member.isEmailVerified()).isTrue(); } + + @Test + @DisplayName("회원 비밀번호 연속 실패 5회까지는 false 반환하고, 6회 시 예외 발생해야 한다") + void 비밀번호_연속_실패_제한_초과() throws PasswordPolicyException, PasswordFailedExceededException { + Member member = Member.builder() + .email("test@example.com") + .fullName("홍길동") + .rawPassword("Password123!") + .build(); + for (int i = 0; i < 5; i++) { + assertThat(member.isMatchedPassword("WrongPassword")).isFalse(); + } + assertThatThrownBy(() -> member.isMatchedPassword("WrongPassword")) + .isInstanceOf(PasswordFailedExceededException.class); + } } \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/social/SocialAccountTest.java b/src/test/java/com/stcom/smartmealtable/domain/social/SocialAccountTest.java new file mode 100644 index 0000000..123c596 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/social/SocialAccountTest.java @@ -0,0 +1,136 @@ +package com.stcom.smartmealtable.domain.social; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.Member; +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SocialAccountTest { + + @Test + @DisplayName("SocialAccount 객체가 빌더를 통해 올바르게 생성된다") + void createSocialAccountWithBuilder() { + // given + Member member = mock(Member.class); + String provider = "KAKAO"; + String providerUserId = "12345"; + String tokenType = "Bearer"; + String accessToken = "access-token-value"; + String refreshToken = "refresh-token-value"; + LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); + + // when + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider(provider) + .providerUserId(providerUserId) + .tokenType(tokenType) + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenExpiresAt(expiresAt) + .build(); + + // then + assertThat(socialAccount.getMember()).isEqualTo(member); + assertThat(socialAccount.getProvider()).isEqualTo(provider); + assertThat(socialAccount.getProviderUserId()).isEqualTo(providerUserId); + assertThat(socialAccount.getTokenType()).isEqualTo(tokenType); + assertThat(socialAccount.getAccessToken()).isEqualTo(accessToken); + assertThat(socialAccount.getRefreshToken()).isEqualTo(refreshToken); + assertThat(socialAccount.getTokenExpiresAt()).isEqualTo(expiresAt); + } + + @Test + @DisplayName("토큰 정보를 업데이트할 수 있다") + void updateTokenInformation() { + // given + Member member = mock(Member.class); + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider("KAKAO") + .providerUserId("12345") + .tokenType("Bearer") + .accessToken("old-access-token") + .refreshToken("old-refresh-token") + .tokenExpiresAt(LocalDateTime.now()) + .build(); + + String newAccessToken = "new-access-token"; + String newRefreshToken = "new-refresh-token"; + LocalDateTime newExpiresAt = LocalDateTime.now().plusHours(2); + + // when + socialAccount.updateToken(newAccessToken, newRefreshToken, newExpiresAt); + + // then + assertThat(socialAccount.getAccessToken()).isEqualTo(newAccessToken); + assertThat(socialAccount.getRefreshToken()).isEqualTo(newRefreshToken); + assertThat(socialAccount.getTokenExpiresAt()).isEqualTo(newExpiresAt); + } + + @Test + @DisplayName("회원 프로필 등록 여부를 확인할 수 있다") + void checkIfProfileIsRegistered() { + // given + Member memberWithProfile = mock(Member.class); + when(memberWithProfile.isProfileRegistered()).thenReturn(true); + + Member memberWithoutProfile = mock(Member.class); + when(memberWithoutProfile.isProfileRegistered()).thenReturn(false); + + SocialAccount accountWithProfile = SocialAccount.builder() + .member(memberWithProfile) + .provider("KAKAO") + .providerUserId("12345") + .build(); + + SocialAccount accountWithoutProfile = SocialAccount.builder() + .member(memberWithoutProfile) + .provider("GOOGLE") + .providerUserId("67890") + .build(); + + // when & then + assertThat(accountWithProfile.isProfileRegistered()).isTrue(); + assertThat(accountWithoutProfile.isProfileRegistered()).isFalse(); + } + + @Test + @DisplayName("다양한 소셜 제공자로 계정을 생성할 수 있다") + void createAccountsWithDifferentProviders() { + // given + Member member = mock(Member.class); + + // when + SocialAccount kakaoAccount = SocialAccount.builder() + .member(member) + .provider("KAKAO") + .providerUserId("kakao-12345") + .build(); + + SocialAccount googleAccount = SocialAccount.builder() + .member(member) + .provider("GOOGLE") + .providerUserId("google-12345") + .build(); + + SocialAccount naverAccount = SocialAccount.builder() + .member(member) + .provider("NAVER") + .providerUserId("naver-12345") + .build(); + + // then + assertThat(kakaoAccount.getProvider()).isEqualTo("KAKAO"); + assertThat(googleAccount.getProvider()).isEqualTo("GOOGLE"); + assertThat(naverAccount.getProvider()).isEqualTo("NAVER"); + + assertThat(kakaoAccount.getProviderUserId()).isEqualTo("kakao-12345"); + assertThat(googleAccount.getProviderUserId()).isEqualTo("google-12345"); + assertThat(naverAccount.getProviderUserId()).isEqualTo("naver-12345"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/domain/term/TermTest.java b/src/test/java/com/stcom/smartmealtable/domain/term/TermTest.java new file mode 100644 index 0000000..7860c30 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/domain/term/TermTest.java @@ -0,0 +1,105 @@ +package com.stcom.smartmealtable.domain.term; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.stcom.smartmealtable.domain.member.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class TermTest { + + @Test + @DisplayName("Term 객체의 필드값이 올바르게 설정된다") + void termFieldsAreSetCorrectly() { + // given + Term term = new Term(); + + // when + ReflectionTestUtils.setField(term, "id", 1L); + ReflectionTestUtils.setField(term, "version", 1); + ReflectionTestUtils.setField(term, "title", "이용약관"); + ReflectionTestUtils.setField(term, "content", "이용약관 내용입니다."); + ReflectionTestUtils.setField(term, "isRequired", true); + + // then + assertThat(term.getId()).isEqualTo(1L); + assertThat(term.getVersion()).isEqualTo(1); + assertThat(term.getTitle()).isEqualTo("이용약관"); + assertThat(term.getContent()).isEqualTo("이용약관 내용입니다."); + assertThat(term.getIsRequired()).isTrue(); + } + + @Test + @DisplayName("필수 약관과 선택 약관을 구분할 수 있다") + void canDistinguishBetweenRequiredAndOptionalTerms() { + // given + Term requiredTerm = new Term(); + Term optionalTerm = new Term(); + + // when + ReflectionTestUtils.setField(requiredTerm, "isRequired", true); + ReflectionTestUtils.setField(optionalTerm, "isRequired", false); + + // then + assertThat(requiredTerm.getIsRequired()).isTrue(); + assertThat(optionalTerm.getIsRequired()).isFalse(); + } +} + +class TermAgreementTest { + + @Test + @DisplayName("TermAgreement 빌더를 통해 객체가 올바르게 생성된다") + void termAgreementIsCreatedCorrectlyUsingBuilder() { + // given + Member member = new Member(); + Term term = new Term(); + + ReflectionTestUtils.setField(term, "id", 1L); + ReflectionTestUtils.setField(term, "title", "이용약관"); + + // when + TermAgreement termAgreement = TermAgreement.builder() + .member(member) + .term(term) + .isAgreed(true) + .build(); + + // then + assertThat(termAgreement.getMember()).isEqualTo(member); + assertThat(termAgreement.getTerm()).isEqualTo(term); + assertThat(termAgreement.getTerm().getId()).isEqualTo(1L); + assertThat(termAgreement.getIsAgreed()).isTrue(); + } + + @Test + @DisplayName("필수 약관 동의 여부를 확인할 수 있다") + void canCheckIfRequiredTermIsAgreed() { + // given + Member member = new Member(); + Term requiredTerm = new Term(); + + ReflectionTestUtils.setField(requiredTerm, "isRequired", true); + + // when + TermAgreement agreedTermAgreement = TermAgreement.builder() + .member(member) + .term(requiredTerm) + .isAgreed(true) + .build(); + + TermAgreement disagreedTermAgreement = TermAgreement.builder() + .member(member) + .term(requiredTerm) + .isAgreed(false) + .build(); + + // then + assertThat(agreedTermAgreement.getTerm().getIsRequired()).isTrue(); + assertThat(agreedTermAgreement.getIsAgreed()).isTrue(); + + assertThat(disagreedTermAgreement.getTerm().getIsRequired()).isTrue(); + assertThat(disagreedTermAgreement.getIsAgreed()).isFalse(); + } +} \ No newline at end of file From 8826de65c16a8146d9108cf2c276d6e5ab30874b Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:54:34 +0900 Subject: [PATCH 116/120] =?UTF-8?q?test:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BudgetServiceTest.java | 141 +++++++ .../service/GroupServiceTest.java | 142 +++++++ .../service/LoginServiceTest.java | 238 +++++++++++ .../MemberCategoryPreferenceServiceTest.java | 186 +++++++++ .../service/MemberProfileServiceTest.java | 394 ++++++++++++++++++ .../service/SocialAccountServiceTest.java | 302 ++++++++++++++ .../service/TermServiceTest.java | 173 ++++++++ 7 files changed, 1576 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java create mode 100644 src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java diff --git a/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java new file mode 100644 index 0000000..b917792 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/BudgetServiceTest.java @@ -0,0 +1,141 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.Budget.DailyBudget; +import com.stcom.smartmealtable.domain.Budget.MonthlyBudget; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.BudgetRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BudgetServiceTest { + + @InjectMocks + private BudgetService budgetService; + + @Mock + private BudgetRepository budgetRepository; + + @Mock + private MemberProfileRepository memberProfileRepository; + + @Test + @DisplayName("회원 프로필 ID로 최근 일일 예산을 조회할 수 있다") + void findRecentDailyBudgetByMemberProfileId() { + // given + Long memberProfileId = 1L; + MemberProfile memberProfile = new MemberProfile(); + DailyBudget dailyBudget = new DailyBudget(memberProfile, BigDecimal.valueOf(10000), LocalDate.now()); + + when(budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId)) + .thenReturn(Optional.of(dailyBudget)); + + // when + DailyBudget result = budgetService.findRecentDailyBudgetByMemberProfileId(memberProfileId); + + // then + assertThat(result).isEqualTo(dailyBudget); + assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(10000)); + } + + @Test + @DisplayName("존재하지 않는 회원 프로필 ID로 일일 예산을 조회하면 예외가 발생한다") + void findRecentDailyBudgetByMemberProfileId_NotFound() { + // given + Long memberProfileId = 999L; + + when(budgetRepository.findFirstDailyBudgetByMemberProfileId(memberProfileId)) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> budgetService.findRecentDailyBudgetByMemberProfileId(memberProfileId)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } + + @Test + @DisplayName("회원 프로필 ID로 최근 월별 예산을 조회할 수 있다") + void findRecentMonthlyBudgetByMemberProfileId() { + // given + Long memberProfileId = 1L; + MemberProfile memberProfile = new MemberProfile(); + MonthlyBudget monthlyBudget = new MonthlyBudget(memberProfile, BigDecimal.valueOf(300000), YearMonth.now()); + + when(budgetRepository.findFirstMonthlyBudgetByMemberProfileId(memberProfileId)) + .thenReturn(Optional.of(monthlyBudget)); + + // when + MonthlyBudget result = budgetService.findRecentMonthlyBudgetByMemberProfileId(memberProfileId); + + // then + assertThat(result).isEqualTo(monthlyBudget); + assertThat(result.getLimit()).isEqualTo(BigDecimal.valueOf(300000)); + } + + @Test + @DisplayName("월별 예산을 저장할 수 있다") + void saveMonthlyBudgetCustom() { + // given + Long memberProfileId = 1L; + Long limit = 300000L; + MemberProfile memberProfile = new MemberProfile(); + + when(memberProfileRepository.findById(memberProfileId)) + .thenReturn(Optional.of(memberProfile)); + + // when + budgetService.saveMonthlyBudgetCustom(memberProfileId, limit); + + // then + verify(budgetRepository).save(any(MonthlyBudget.class)); + } + + @Test + @DisplayName("일일 예산을 저장할 수 있다") + void saveDailyBudgetCustom() { + // given + Long memberProfileId = 1L; + Long limit = 10000L; + MemberProfile memberProfile = new MemberProfile(); + + when(memberProfileRepository.findById(memberProfileId)) + .thenReturn(Optional.of(memberProfile)); + + // when + budgetService.saveDailyBudgetCustom(memberProfileId, limit); + + // then + verify(budgetRepository).save(any(DailyBudget.class)); + } + + @Test + @DisplayName("존재하지 않는 회원 프로필 ID로 예산을 저장하면 예외가 발생한다") + void saveBudget_NotFound() { + // given + Long memberProfileId = 999L; + Long limit = 10000L; + + when(memberProfileRepository.findById(memberProfileId)) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> budgetService.saveDailyBudgetCustom(memberProfileId, limit)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("존재하지 않는 프로필로 접근"); + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java new file mode 100644 index 0000000..8be1f60 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/GroupServiceTest.java @@ -0,0 +1,142 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.CompanyGroup; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.IndustryType; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.repository.GroupRepository; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Limit; + +@ExtendWith(MockitoExtension.class) +class GroupServiceTest { + + @Mock + private GroupRepository groupRepository; + + @InjectMocks + private GroupServiceImpl groupService; + + @Test + @DisplayName("ID로 그룹을 찾을 수 있어야 한다") + void findGroupByGroupId() { + // given + Long groupId = 1L; + + CompanyGroup companyGroup = new CompanyGroup(); + Address address = createAddress("서울시 강남구 테헤란로 123"); + + // 리플렉션으로 private 필드 설정 + org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "id", groupId); + org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "name", "IT 회사"); + org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "address", address); + org.springframework.test.util.ReflectionTestUtils.setField(companyGroup, "industryType", IndustryType.IT); + + when(groupRepository.findById(groupId)).thenReturn(Optional.of(companyGroup)); + + // when + Group foundGroup = groupService.findGroupByGroupId(groupId); + + // then + assertThat(foundGroup).isEqualTo(companyGroup); + assertThat(foundGroup.getName()).isEqualTo("IT 회사"); + assertThat(foundGroup.getTypeName()).isEqualTo("IT"); + assertThat(foundGroup.getAddress().getRoadAddress()).isEqualTo("서울시 강남구 테헤란로 123"); + verify(groupRepository, times(1)).findById(groupId); + } + + @Test + @DisplayName("존재하지 않는 그룹 ID로 조회 시 예외가 발생해야 한다") + void findGroupByGroupIdNotFound() { + // given + Long groupId = 999L; + when(groupRepository.findById(groupId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> groupService.findGroupByGroupId(groupId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("키워드로 그룹을 검색할 수 있어야 한다") + void findGroupsByKeyword() { + // given + String keyword = "학교"; + + SchoolGroup schoolGroup1 = createSchoolGroup(1L, "서울대학교", SchoolType.UNIVERSITY_FOUR_YEAR, + createAddress("서울시 관악구 관악로 1")); + + SchoolGroup schoolGroup2 = createSchoolGroup(2L, "부산대학교", SchoolType.UNIVERSITY_FOUR_YEAR, + createAddress("부산시 금정구 부산대학로 63번길 2")); + + List expectedGroups = Arrays.asList(schoolGroup1, schoolGroup2); + + when(groupRepository.findByNameContaining(keyword, Limit.of(10))).thenReturn(expectedGroups); + + // when + List foundGroups = groupService.findGroupsByKeyword(keyword); + + // then + assertThat(foundGroups).hasSize(2); + assertThat(foundGroups).containsExactly(schoolGroup1, schoolGroup2); + + assertThat(foundGroups.get(0).getName()).isEqualTo("서울대학교"); + assertThat(foundGroups.get(0).getTypeName()).isEqualTo("UNIVERSITY_FOUR_YEAR"); + + assertThat(foundGroups.get(1).getName()).isEqualTo("부산대학교"); + assertThat(foundGroups.get(1).getAddress().getRoadAddress()).isEqualTo("부산시 금정구 부산대학로 63번길 2"); + + verify(groupRepository, times(1)).findByNameContaining(keyword, Limit.of(10)); + } + + @Test + @DisplayName("키워드 검색 결과가 없을 경우 빈 리스트를 반환해야 한다") + void findGroupsByKeywordNoResult() { + // given + String keyword = "존재하지 않는 키워드"; + when(groupRepository.findByNameContaining(keyword, Limit.of(10))).thenReturn(List.of()); + + // when + List foundGroups = groupService.findGroupsByKeyword(keyword); + + // then + assertThat(foundGroups).isEmpty(); + verify(groupRepository, times(1)).findByNameContaining(keyword, Limit.of(10)); + } + + // 테스트용 주소 생성 헬퍼 메소드 + private Address createAddress(String roadAddress) { + Address address = new Address(); + org.springframework.test.util.ReflectionTestUtils.setField(address, "roadAddress", roadAddress); + return address; + } + + // 테스트용 학교 그룹 생성 헬퍼 메소드 + private SchoolGroup createSchoolGroup(Long id, String name, SchoolType schoolType, Address address) { + SchoolGroup group = new SchoolGroup(); + org.springframework.test.util.ReflectionTestUtils.setField(group, "id", id); + org.springframework.test.util.ReflectionTestUtils.setField(group, "name", name); + org.springframework.test.util.ReflectionTestUtils.setField(group, "address", address); + org.springframework.test.util.ReflectionTestUtils.setField(group, "schoolType", schoolType); + return group; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java new file mode 100644 index 0000000..86b3858 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/LoginServiceTest.java @@ -0,0 +1,238 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.exception.PasswordPolicyException; +import com.stcom.smartmealtable.exception.PasswordFailedExceededException; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import com.stcom.smartmealtable.service.dto.AuthResultDto; +import java.time.LocalDateTime; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class LoginServiceTest { + + @Mock + private MemberRepository memberRepository; + + @Mock + private SocialAccountRepository socialAccountRepository; + + @InjectMocks + private LoginService loginService; + + @Captor + private ArgumentCaptor memberCaptor; + + @Captor + private ArgumentCaptor socialAccountCaptor; + + @Test + @DisplayName("이메일과 비밀번호로 로그인이 가능해야 한다") + void loginWithEmail() throws PasswordFailedExceededException, PasswordPolicyException { + // given + String email = "test@example.com"; + String password = "Password123!"; + + Member member = createMember(1L, email, password); + MemberProfile profile = mock(MemberProfile.class); + when(profile.getId()).thenReturn(10L); + + // 프로필이 등록되어 있지 않은 경우 + ReflectionTestUtils.setField(member, "memberProfile", profile); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + + // when + AuthResultDto result = loginService.loginWithEmail(email, password); + + // then + assertThat(result.getMemberId()).isEqualTo(1L); + assertThat(result.getProfileId()).isEqualTo(10L); + assertThat(result.isNewUser()).isFalse(); // 프로필이 있으므로 새 사용자가 아님 + + verify(memberRepository, times(1)).findByEmail(email); + } + + @Test + @DisplayName("프로필이 없는 경우 신규 사용자로 처리해야 한다") + void loginWithEmailNewUser() throws PasswordFailedExceededException, PasswordPolicyException { + // given + String email = "new@example.com"; + String password = "Password123!"; + + Member member = createMember(2L, email, password); + // 프로필이 등록되어 있지 않음 + + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + + // when + AuthResultDto result = loginService.loginWithEmail(email, password); + + // then + assertThat(result.getMemberId()).isEqualTo(2L); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); // 프로필이 없으므로 신규 사용자 + } + + @Test + @DisplayName("존재하지 않는 이메일로 로그인 시도 시 예외가 발생해야 한다") + void loginWithNonExistingEmail() { + // given + String email = "nonexisting@example.com"; + String password = "Password123!"; + + when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail(email, password)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다."); + } + + @Test + @DisplayName("잘못된 비밀번호로 로그인 시도 시 예외가 발생해야 한다") + void loginWithWrongPassword() throws PasswordPolicyException { + // given + String email = "test@example.com"; + String correctPassword = "Password123!"; + String wrongPassword = "WrongPassword123!"; + + Member member = createMember(1L, email, correctPassword); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + + // when & then + assertThatThrownBy(() -> loginService.loginWithEmail(email, wrongPassword)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("비밀번호가 일치하지 않습니다"); + } + + @Test + @DisplayName("소셜 로그인 - 기존 회원인 경우") + void socialLoginExistingMember() throws PasswordPolicyException { + // given + String email = "test@example.com"; + String provider = "KAKAO"; + String providerUserId = "12345"; + + TokenDto tokenDto = createTokenDto(email, provider, providerUserId); + Member member = createMember(1L, email, null); + + SocialAccount existingSocialAccount = SocialAccount.builder() + .member(member) + .provider(provider) + .providerUserId(providerUserId) + .accessToken("old-token") + .refreshToken("old-refresh-token") + .tokenExpiresAt(LocalDateTime.now()) + .build(); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(member)); + when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.of(existingSocialAccount)); + when(socialAccountRepository.findProfileIdByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.of(10L)); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + assertThat(result.getMemberId()).isEqualTo(1L); + assertThat(result.getProfileId()).isEqualTo(10L); + assertThat(result.isNewUser()).isFalse(); + + assertThat(existingSocialAccount.getAccessToken()).isEqualTo("access-token-value"); + assertThat(existingSocialAccount.getRefreshToken()).isEqualTo("refresh-token-value"); + } + + @Test + @DisplayName("소셜 로그인 - 신규 회원인 경우") + void socialLoginNewMember() throws PasswordPolicyException { + // given + String email = "new@example.com"; + String provider = "GOOGLE"; + String providerUserId = "67890"; + + TokenDto tokenDto = createTokenDto(email, provider, providerUserId); + Member newMember = createMember(2L, email, null); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); + when(memberRepository.save(any())).thenReturn(newMember); + when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.empty()); + when(socialAccountRepository.save(any())).thenAnswer(invocation -> { + SocialAccount account = invocation.getArgument(0); + ReflectionTestUtils.setField(account, "id", 1L); + return account; + }); + when(socialAccountRepository.findProfileIdByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.empty()); + + // when + AuthResultDto result = loginService.socialLogin(tokenDto); + + // then + verify(memberRepository, times(1)).save(memberCaptor.capture()); + verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); + + Member savedMember = memberCaptor.getValue(); + SocialAccount savedAccount = socialAccountCaptor.getValue(); + + assertThat(savedMember.getEmail()).isEqualTo(email); + assertThat(savedAccount.getProvider()).isEqualTo(provider); + assertThat(savedAccount.getProviderUserId()).isEqualTo(providerUserId); + + assertThat(result.getMemberId()).isEqualTo(2L); + assertThat(result.getProfileId()).isNull(); + assertThat(result.isNewUser()).isTrue(); + } + + private Member createMember(Long id, String email, String rawPassword) throws PasswordPolicyException { + Member member; + if (rawPassword != null) { + member = Member.builder() + .email(email) + .rawPassword(rawPassword) + .build(); + } else { + member = new Member(email); + } + ReflectionTestUtils.setField(member, "id", id); + return member; + } + + private TokenDto createTokenDto(String email, String provider, String providerUserId) { + TokenDto tokenDto = new TokenDto(); + + tokenDto.setEmail(email); + tokenDto.setProvider(provider); + tokenDto.setProviderUserId(providerUserId); + tokenDto.setTokenType("Bearer"); + tokenDto.setAccessToken("access-token-value"); + tokenDto.setRefreshToken("refresh-token-value"); + tokenDto.setExpiresIn(3600); + + return tokenDto; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java new file mode 100644 index 0000000..10f852a --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberCategoryPreferenceServiceTest.java @@ -0,0 +1,186 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.food.FoodCategory; +import com.stcom.smartmealtable.domain.food.MemberCategoryPreference; +import com.stcom.smartmealtable.domain.food.PreferenceType; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.repository.FoodCategoryRepository; +import com.stcom.smartmealtable.repository.MemberCategoryPreferenceRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class MemberCategoryPreferenceServiceTest { + + @Mock + private MemberCategoryPreferenceRepository preferenceRepository; + + @Mock + private FoodCategoryRepository categoryRepository; + + @Mock + private MemberProfileRepository profileRepository; + + @InjectMocks + private MemberCategoryPreferenceService preferenceService; + + @Captor + private ArgumentCaptor preferenceCaptor; + + private MemberProfile profile; + private FoodCategory koreanFood; + private FoodCategory westernFood; + private FoodCategory japaneseFood; + private FoodCategory chineseFood; + + @BeforeEach + void setUp() { + // 테스트 데이터 셋업 + profile = new MemberProfile(); + ReflectionTestUtils.setField(profile, "id", 1L); + + koreanFood = createFoodCategory(1L, "한식"); + westernFood = createFoodCategory(2L, "양식"); + japaneseFood = createFoodCategory(3L, "일식"); + chineseFood = createFoodCategory(4L, "중식"); + } + + @Test + @DisplayName("회원 음식 선호도를 저장할 수 있어야 한다") + void savePreferences() { + // given + Long profileId = 1L; + List liked = Arrays.asList(1L, 2L); // 한식, 양식 선호 + List disliked = Arrays.asList(3L); // 일식 비선호 + + when(profileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(categoryRepository.findById(1L)).thenReturn(Optional.of(koreanFood)); + when(categoryRepository.findById(2L)).thenReturn(Optional.of(westernFood)); + when(categoryRepository.findById(3L)).thenReturn(Optional.of(japaneseFood)); + doNothing().when(preferenceRepository).deleteByMemberProfile_Id(profileId); + when(preferenceRepository.save(any(MemberCategoryPreference.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // when + preferenceService.savePreferences(profileId, liked, disliked); + + // then + verify(profileRepository, times(1)).findById(profileId); + verify(preferenceRepository, times(1)).deleteByMemberProfile_Id(profileId); + verify(categoryRepository, times(3)).findById(anyLong()); + verify(preferenceRepository, times(3)).save(preferenceCaptor.capture()); + + List savedPreferences = preferenceCaptor.getAllValues(); + assertThat(savedPreferences).hasSize(3); + + // 선호 음식 검증 + assertThat(savedPreferences.get(0).getType()).isEqualTo(PreferenceType.LIKE); + assertThat(savedPreferences.get(0).getCategory().getName()).isEqualTo("한식"); + assertThat(savedPreferences.get(0).getPriority()).isEqualTo(1); + + assertThat(savedPreferences.get(1).getType()).isEqualTo(PreferenceType.LIKE); + assertThat(savedPreferences.get(1).getCategory().getName()).isEqualTo("양식"); + assertThat(savedPreferences.get(1).getPriority()).isEqualTo(2); + + // 비선호 음식 검증 + assertThat(savedPreferences.get(2).getType()).isEqualTo(PreferenceType.DISLIKE); + assertThat(savedPreferences.get(2).getCategory().getName()).isEqualTo("일식"); + assertThat(savedPreferences.get(2).getPriority()).isEqualTo(1); + } + + @Test + @DisplayName("존재하지 않는 프로필로 음식 선호도 저장시 예외가 발생해야 한다") + void savePreferencesWithNonExistingProfile() { + // given + Long profileId = 999L; + List liked = Arrays.asList(1L); + List disliked = List.of(); + + when(profileRepository.findById(profileId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> preferenceService.savePreferences(profileId, liked, disliked)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 카테고리로 음식 선호도 저장시 예외가 발생해야 한다") + void savePreferencesWithNonExistingCategory() { + // given + Long profileId = 1L; + List liked = Arrays.asList(999L); + List disliked = List.of(); + + when(profileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(categoryRepository.findById(999L)).thenReturn(Optional.empty()); + doNothing().when(preferenceRepository).deleteByMemberProfile_Id(profileId); + + // when & then + assertThatThrownBy(() -> preferenceService.savePreferences(profileId, liked, disliked)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 카테고리입니다"); + } + + @Test + @DisplayName("프로필 ID로 음식 선호도를 조회할 수 있어야 한다") + void getPreferences() { + // given + Long profileId = 1L; + MemberCategoryPreference pref1 = createPreference(profile, koreanFood, PreferenceType.LIKE, 1); + MemberCategoryPreference pref2 = createPreference(profile, westernFood, PreferenceType.LIKE, 2); + MemberCategoryPreference pref3 = createPreference(profile, japaneseFood, PreferenceType.DISLIKE, 1); + + List expectedPreferences = Arrays.asList(pref1, pref2, pref3); + + when(preferenceRepository.findDefaultByMemberProfileId(profileId)).thenReturn(expectedPreferences); + + // when + List foundPreferences = preferenceService.getPreferences(profileId); + + // then + assertThat(foundPreferences).hasSize(3); + assertThat(foundPreferences).isEqualTo(expectedPreferences); + + verify(preferenceRepository, times(1)).findDefaultByMemberProfileId(profileId); + } + + private FoodCategory createFoodCategory(Long id, String name) { + FoodCategory foodCategory = new FoodCategory(); + ReflectionTestUtils.setField(foodCategory, "id", id); + ReflectionTestUtils.setField(foodCategory, "name", name); + return foodCategory; + } + + private MemberCategoryPreference createPreference(MemberProfile profile, FoodCategory category, + PreferenceType type, Integer priority) { + MemberCategoryPreference preference = MemberCategoryPreference.builder() + .memberProfile(profile) + .category(category) + .type(type) + .priority(priority) + .build(); + ReflectionTestUtils.setField(preference, "id", Long.valueOf(priority)); + return preference; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java new file mode 100644 index 0000000..70995c3 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/MemberProfileServiceTest.java @@ -0,0 +1,394 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.Address.AddressEntity; +import com.stcom.smartmealtable.domain.Address.AddressType; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.member.MemberProfile; +import com.stcom.smartmealtable.domain.member.MemberType; +import com.stcom.smartmealtable.repository.AddressEntityRepository; +import com.stcom.smartmealtable.repository.GroupRepository; +import com.stcom.smartmealtable.repository.MemberProfileRepository; +import com.stcom.smartmealtable.repository.MemberRepository; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class MemberProfileServiceTest { + + @Mock + private MemberProfileRepository memberProfileRepository; + + @Mock + private GroupRepository groupRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private AddressEntityRepository addressEntityRepository; + + @InjectMocks + private MemberProfileService profileService; + + @Captor + private ArgumentCaptor profileCaptor; + + @Captor + private ArgumentCaptor addressEntityCaptor; + + private Member member; + private Group group; + private Address address; + private MemberProfile profile; + + @BeforeEach + void setUp() { + // 테스트 데이터 셋업 + member = new Member("test@example.com"); + ReflectionTestUtils.setField(member, "id", 1L); + + group = new SchoolGroup(); + ReflectionTestUtils.setField(group, "id", 1L); + ReflectionTestUtils.setField(group, "name", "서울대학교"); + ReflectionTestUtils.setField(group, "schoolType", SchoolType.UNIVERSITY_FOUR_YEAR); + + address = createAddress("서울특별시 관악구 관악로 1", "서울특별시 관악구", "101호", 37.459, 126.952); + } + + @Test + @DisplayName("프로필 ID로 프로필 정보를 조회할 수 있어야 한다") + void getProfileFetch() { + // given + Long profileId = 1L; + profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); + + when(memberProfileRepository.findMemberProfileEntityGraphById(profileId)) + .thenReturn(Optional.of(profile)); + + // when + MemberProfile fetchedProfile = profileService.getProfileFetch(profileId); + + // then + assertThat(fetchedProfile).isEqualTo(profile); + assertThat(fetchedProfile.getNickName()).isEqualTo("닉네임"); + assertThat(fetchedProfile.getMember()).isEqualTo(member); + assertThat(fetchedProfile.getType()).isEqualTo(MemberType.STUDENT); + assertThat(fetchedProfile.getGroup()).isEqualTo(group); + + verify(memberProfileRepository).findMemberProfileEntityGraphById(profileId); + } + + @Test + @DisplayName("프로필을 생성할 수 있어야 한다 - 그룹 있음") + void createProfileWithGroup() { + // given + String nickName = "새닉네임"; + Long memberId = 1L; + MemberType type = MemberType.STUDENT; + Long groupId = 1L; + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(groupRepository.getReferenceById(groupId)).thenReturn(group); + when(memberProfileRepository.save(any(MemberProfile.class))).thenAnswer(invocation -> { + MemberProfile savedProfile = invocation.getArgument(0); + ReflectionTestUtils.setField(savedProfile, "id", 1L); + return savedProfile; + }); + + // when + profileService.createProfile(nickName, memberId, type, groupId); + + // then + verify(memberRepository).findById(memberId); + verify(groupRepository).getReferenceById(groupId); + verify(memberProfileRepository).save(profileCaptor.capture()); + + MemberProfile savedProfile = profileCaptor.getValue(); + assertThat(savedProfile.getNickName()).isEqualTo(nickName); + assertThat(savedProfile.getMember()).isEqualTo(member); + assertThat(savedProfile.getType()).isEqualTo(type); + assertThat(savedProfile.getGroup()).isEqualTo(group); + } + + @Test + @DisplayName("프로필을 생성할 수 있어야 한다 - 그룹 없음") + void createProfileWithoutGroup() { + // given + String nickName = "새닉네임"; + Long memberId = 1L; + MemberType type = MemberType.OTHER; + Long groupId = null; + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(memberProfileRepository.save(any(MemberProfile.class))).thenAnswer(invocation -> { + MemberProfile savedProfile = invocation.getArgument(0); + ReflectionTestUtils.setField(savedProfile, "id", 1L); + return savedProfile; + }); + + // when + profileService.createProfile(nickName, memberId, type, groupId); + + // then + verify(memberRepository).findById(memberId); + verify(memberProfileRepository).save(profileCaptor.capture()); + + MemberProfile savedProfile = profileCaptor.getValue(); + assertThat(savedProfile.getNickName()).isEqualTo(nickName); + assertThat(savedProfile.getMember()).isEqualTo(member); + assertThat(savedProfile.getType()).isEqualTo(type); + assertThat(savedProfile.getGroup()).isNull(); + } + + @Test + @DisplayName("프로필 정보를 변경할 수 있어야 한다") + void changeProfile() { + // given + Long profileId = 1L; + String newNickName = "변경된닉네임"; + MemberType newType = MemberType.WORKER; + Long newGroupId = 2L; + + profile = createProfile(profileId, "원래닉네임", member, MemberType.STUDENT, group); + + Group newGroup = new SchoolGroup(); + ReflectionTestUtils.setField(newGroup, "id", 2L); + ReflectionTestUtils.setField(newGroup, "name", "회사"); + + when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(groupRepository.getReferenceById(newGroupId)).thenReturn(newGroup); + + // when + profileService.changeProfile(profileId, newNickName, newType, newGroupId); + + // then + verify(memberProfileRepository).findById(profileId); + verify(groupRepository).getReferenceById(newGroupId); + + assertThat(profile.getNickName()).isEqualTo(newNickName); + assertThat(profile.getType()).isEqualTo(newType); + assertThat(profile.getGroup()).isEqualTo(newGroup); + } + + @Test + @DisplayName("새 주소를 추가할 수 있어야 한다") + void saveNewAddress() { + // given + Long profileId = 1L; + String alias = "집"; + AddressType addressType = AddressType.HOME; + + profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); + ReflectionTestUtils.setField(profile, "addressHistory", new ArrayList<>()); + + when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(addressEntityRepository.save(any(AddressEntity.class))).thenAnswer(invocation -> { + AddressEntity savedAddress = invocation.getArgument(0); + ReflectionTestUtils.setField(savedAddress, "id", 1L); + return savedAddress; + }); + + // when + profileService.saveNewAddress(profileId, address, alias, addressType); + + // then + verify(memberProfileRepository).findById(profileId); + verify(addressEntityRepository).save(addressEntityCaptor.capture()); + + AddressEntity savedAddressEntity = addressEntityCaptor.getValue(); + assertThat(savedAddressEntity.getAddress()).isEqualTo(address); + assertThat(savedAddressEntity.getAlias()).isEqualTo(alias); + assertThat(savedAddressEntity.getType()).isEqualTo(addressType); + assertThat(profile.getAddressHistory()).hasSize(1); + } + + @Test + @DisplayName("주소 정보를 변경할 수 있어야 한다") + void changeAddress() { + // given + Long profileId = 1L; + Long addressEntityId = 1L; + String newAlias = "새집"; + AddressType newType = AddressType.HOME; + + profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); + + AddressEntity addressEntity = AddressEntity.builder() + .address(address) + .alias("구집") + .type(AddressType.ETC) + .build(); + ReflectionTestUtils.setField(addressEntity, "id", addressEntityId); + + List addresses = new ArrayList<>(); + addresses.add(addressEntity); + ReflectionTestUtils.setField(profile, "addressHistory", addresses); + + Address newAddress = createAddress("서울시 서초구 서초대로 123", "서초구", "202호", 37.5, 127.0); + + when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(addressEntityRepository.findById(addressEntityId)).thenReturn(Optional.of(addressEntity)); + + // when + profileService.changeAddress(profileId, addressEntityId, newAddress, newAlias, newType); + + // then + verify(memberProfileRepository).findById(profileId); + verify(addressEntityRepository).findById(addressEntityId); + + // 주소 정보가 업데이트되었는지 확인 + assertThat(addressEntity.getAddress()).isEqualTo(newAddress); + assertThat(addressEntity.getAlias()).isEqualTo(newAlias); + assertThat(addressEntity.getType()).isEqualTo(newType); + } + + @Test + @DisplayName("주소를 삭제할 수 있어야 한다") + void deleteAddress() { + // given + Long profileId = 1L; + Long addressEntityId = 1L; + + profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); + + AddressEntity addressEntity = AddressEntity.builder() + .address(address) + .alias("집") + .type(AddressType.HOME) + .build(); + ReflectionTestUtils.setField(addressEntity, "id", addressEntityId); + + // 주소에 primary=true 설정 + ReflectionTestUtils.setField(addressEntity, "primary", true); + + List addresses = new ArrayList<>(); + addresses.add(addressEntity); + + // 두번째 주소 추가 + AddressEntity secondAddress = AddressEntity.builder() + .address(createAddress("서울시 강남구", "강남구", "301호", 37.4, 127.1)) + .alias("회사") + .type(AddressType.OFFICE) + .build(); + ReflectionTestUtils.setField(secondAddress, "id", 2L); + addresses.add(secondAddress); + + ReflectionTestUtils.setField(profile, "addressHistory", addresses); + + when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + when(addressEntityRepository.findById(addressEntityId)).thenReturn(Optional.of(addressEntity)); + + // when + profileService.deleteAddress(profileId, addressEntityId); + + // then + verify(memberProfileRepository).findById(profileId); + verify(addressEntityRepository).findById(addressEntityId); + + // 주소가 삭제되었는지 확인 + assertThat(profile.getAddressHistory()).hasSize(1); + assertThat(profile.getAddressHistory().get(0)).isEqualTo(secondAddress); + } + + @Test + @DisplayName("기본 예산을 설정할 수 있어야 한다") + void registerDefaultBudgets() { + // given + Long profileId = 1L; + Long dailyLimit = 10000L; + Long monthlyLimit = 300000L; + + profile = createProfile(profileId, "닉네임", member, MemberType.STUDENT, group); + + when(memberProfileRepository.findById(profileId)).thenReturn(Optional.of(profile)); + + // when + profileService.registerDefaultBudgets(profileId, dailyLimit, monthlyLimit); + + // then + verify(memberProfileRepository).findById(profileId); + + // 기본 예산 설정 로직은 실제로는 MemberProfile 내부 메서드에 있으므로 + // 이 테스트에서는 메서드 호출 여부만 확인 + } + + @Test + @DisplayName("존재하지 않는 프로필 ID로 조회 시 예외가 발생해야 한다") + void getProfileFetchNotFound() { + // given + Long nonExistingProfileId = 999L; + when(memberProfileRepository.findMemberProfileEntityGraphById(nonExistingProfileId)) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> profileService.getProfileFetch(nonExistingProfileId)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("존재하지 않는 프로필입니다"); + } + + @Test + @DisplayName("존재하지 않는 회원 ID로 프로필 생성 시 예외가 발생해야 한다") + void createProfileWithNonExistingMember() { + // given + Long nonExistingMemberId = 999L; + when(memberRepository.findById(nonExistingMemberId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> profileService.createProfile("닉네임", nonExistingMemberId, MemberType.STUDENT, 1L)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + private MemberProfile createProfile(Long id, String nickName, Member member, MemberType type, Group group) { + MemberProfile profile = MemberProfile.builder() + .nickName(nickName) + .member(member) + .type(type) + .group(group) + .build(); + ReflectionTestUtils.setField(profile, "id", id); + return profile; + } + + private Address createAddress(String roadAddress, String detailAddress, String alias, + double latitude, double longitude) { + Address address = Address.builder() + .roadAddress(roadAddress) + .detailAddress(detailAddress) + .latitude(latitude) + .longitude(longitude) + .build(); + return address; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java new file mode 100644 index 0000000..c8bb193 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/SocialAccountServiceTest.java @@ -0,0 +1,302 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.social.SocialAccount; +import com.stcom.smartmealtable.infrastructure.dto.TokenDto; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.SocialAccountRepository; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class SocialAccountServiceTest { + + @Mock + private SocialAccountRepository socialAccountRepository; + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private SocialAccountService socialAccountService; + + @Captor + private ArgumentCaptor memberCaptor; + + @Captor + private ArgumentCaptor socialAccountCaptor; + + @Test + @DisplayName("새 회원 생성 및 소셜 계정 연결이 가능해야 한다") + void createNewMemberAndLinkSocialAccount() { + // given + TokenDto tokenDto = createTokenDto("test@example.com", "KAKAO", "12345"); + + when(memberRepository.save(any(Member.class))).thenAnswer(invocation -> { + Member member = invocation.getArgument(0); + ReflectionTestUtils.setField(member, "id", 1L); + return member; + }); + + when(socialAccountRepository.save(any(SocialAccount.class))).thenAnswer(invocation -> { + SocialAccount account = invocation.getArgument(0); + ReflectionTestUtils.setField(account, "id", 1L); + return account; + }); + + // when + socialAccountService.createNewMemberAndLinkSocialAccount(tokenDto); + + // then + verify(memberRepository, times(1)).save(memberCaptor.capture()); + verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); + + Member savedMember = memberCaptor.getValue(); + SocialAccount savedAccount = socialAccountCaptor.getValue(); + + assertThat(savedMember.getEmail()).isEqualTo("test@example.com"); + assertThat(savedAccount.getMember()).isEqualTo(savedMember); + assertThat(savedAccount.getProvider()).isEqualTo("KAKAO"); + assertThat(savedAccount.getProviderUserId()).isEqualTo("12345"); + assertThat(savedAccount.getAccessToken()).isEqualTo("access-token-value"); + assertThat(savedAccount.getRefreshToken()).isEqualTo("refresh-token-value"); + } + + @Test + @DisplayName("기존 회원에 소셜 계정 연결이 가능해야 한다") + void linkSocialAccount() { + // given + String email = "test@example.com"; + TokenDto tokenDto = createTokenDto(email, "GOOGLE", "67890"); + + Member existingMember = new Member(email); + ReflectionTestUtils.setField(existingMember, "id", 1L); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.of(existingMember)); + when(socialAccountRepository.save(any(SocialAccount.class))).thenAnswer(invocation -> { + SocialAccount account = invocation.getArgument(0); + ReflectionTestUtils.setField(account, "id", 2L); + return account; + }); + + // when + socialAccountService.linkSocialAccount(tokenDto); + + // then + verify(memberRepository, times(1)).findByEmail(email); + verify(socialAccountRepository, times(1)).save(socialAccountCaptor.capture()); + + SocialAccount savedAccount = socialAccountCaptor.getValue(); + + assertThat(savedAccount.getMember()).isEqualTo(existingMember); + assertThat(savedAccount.getProvider()).isEqualTo("GOOGLE"); + assertThat(savedAccount.getProviderUserId()).isEqualTo("67890"); + assertThat(savedAccount.getAccessToken()).isEqualTo("access-token-value"); + } + + @Test + @DisplayName("존재하지 않는 이메일로 소셜 계정 연결 시 예외가 발생해야 한다") + void linkSocialAccountWithNonExistingEmail() { + // given + String email = "nonexisting@example.com"; + TokenDto tokenDto = createTokenDto(email, "KAKAO", "12345"); + + when(memberRepository.findByEmail(email)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> socialAccountService.linkSocialAccount(tokenDto)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("회원이 null일 수는 없습니다"); + } + + @Test + @DisplayName("Provider와 ProviderUserId로 소셜 계정을 찾을 수 있어야 한다") + void findSocialAccount() { + // given + String provider = "KAKAO"; + String providerUserId = "12345"; + + Member member = new Member("test@example.com"); + ReflectionTestUtils.setField(member, "id", 1L); + + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider(provider) + .providerUserId(providerUserId) + .build(); + ReflectionTestUtils.setField(socialAccount, "id", 1L); + + when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.of(socialAccount)); + + // when + SocialAccount foundAccount = socialAccountService.findSocialAccount(provider, providerUserId); + + // then + assertThat(foundAccount).isNotNull(); + assertThat(foundAccount.getProvider()).isEqualTo(provider); + assertThat(foundAccount.getProviderUserId()).isEqualTo(providerUserId); + assertThat(foundAccount.getMember()).isEqualTo(member); + + verify(socialAccountRepository, times(1)).findByProviderAndProviderUserId(provider, providerUserId); + } + + @Test + @DisplayName("존재하지 않는 소셜 계정 찾기 시 null을 반환해야 한다") + void findNonExistingSocialAccount() { + // given + String provider = "KAKAO"; + String providerUserId = "nonexisting"; + + when(socialAccountRepository.findByProviderAndProviderUserId(provider, providerUserId)) + .thenReturn(Optional.empty()); + + // when + SocialAccount foundAccount = socialAccountService.findSocialAccount(provider, providerUserId); + + // then + assertThat(foundAccount).isNull(); + } + + @Test + @DisplayName("신규 사용자 여부를 확인할 수 있어야 한다") + void isNewUser() { + // given + String existingProvider = "KAKAO"; + String existingProviderId = "12345"; + + String newProvider = "GOOGLE"; + String newProviderId = "67890"; + + when(socialAccountRepository.findByProviderAndProviderUserId(existingProvider, existingProviderId)) + .thenReturn(Optional.of(new SocialAccount())); + + when(socialAccountRepository.findByProviderAndProviderUserId(newProvider, newProviderId)) + .thenReturn(Optional.empty()); + + // when + boolean existingUserResult = socialAccountService.isNewUser(existingProvider, existingProviderId); + boolean newUserResult = socialAccountService.isNewUser(newProvider, newProviderId); + + // then + assertThat(existingUserResult).isFalse(); + assertThat(newUserResult).isTrue(); + } + + @Test + @DisplayName("토큰 정보를 업데이트할 수 있어야 한다") + void updateToken() { + // given + Long socialAccountId = 1L; + + Member member = new Member("test@example.com"); + SocialAccount socialAccount = SocialAccount.builder() + .member(member) + .provider("KAKAO") + .providerUserId("12345") + .tokenType("Bearer") + .accessToken("old-access-token") + .refreshToken("old-refresh-token") + .tokenExpiresAt(LocalDateTime.now()) + .build(); + ReflectionTestUtils.setField(socialAccount, "id", socialAccountId); + + String newAccessToken = "new-access-token"; + String newRefreshToken = "new-refresh-token"; + LocalDateTime newExpiresAt = LocalDateTime.now().plusHours(1); + + when(socialAccountRepository.findById(socialAccountId)).thenReturn(Optional.of(socialAccount)); + + // when + socialAccountService.updateToken(socialAccountId, newAccessToken, newRefreshToken, newExpiresAt); + + // then + verify(socialAccountRepository, times(1)).findById(socialAccountId); + + assertThat(socialAccount.getAccessToken()).isEqualTo(newAccessToken); + assertThat(socialAccount.getRefreshToken()).isEqualTo(newRefreshToken); + assertThat(socialAccount.getTokenExpiresAt()).isEqualTo(newExpiresAt); + } + + @Test + @DisplayName("존재하지 않는 소셜 계정 ID로 토큰 업데이트 시 예외가 발생해야 한다") + void updateTokenWithNonExistingId() { + // given + Long nonExistingId = 999L; + + when(socialAccountRepository.findById(nonExistingId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> socialAccountService.updateToken( + nonExistingId, "new-token", "new-refresh-token", LocalDateTime.now())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("확인되지 않은 계정입니다"); + } + + @Test + @DisplayName("회원 ID로 연결된 모든 소셜 Provider 목록을 가져올 수 있어야 한다") + void findAllProviders() { + // given + Long memberId = 1L; + + Member member = new Member("test@example.com"); + + SocialAccount kakaoAccount = SocialAccount.builder() + .member(member) + .provider("KAKAO") + .providerUserId("kakao-12345") + .build(); + + SocialAccount googleAccount = SocialAccount.builder() + .member(member) + .provider("GOOGLE") + .providerUserId("google-67890") + .build(); + + List socialAccounts = Arrays.asList(kakaoAccount, googleAccount); + + when(socialAccountRepository.findAllByMemberId(memberId)).thenReturn(socialAccounts); + + // when + List providers = socialAccountService.findAllProviders(memberId); + + // then + assertThat(providers).hasSize(2); + assertThat(providers).containsExactly("KAKAO", "GOOGLE"); + + verify(socialAccountRepository, times(1)).findAllByMemberId(memberId); + } + + private TokenDto createTokenDto(String email, String provider, String providerUserId) { + TokenDto tokenDto = new TokenDto(); + + tokenDto.setEmail(email); + tokenDto.setProvider(provider); + tokenDto.setProviderUserId(providerUserId); + tokenDto.setTokenType("Bearer"); + tokenDto.setAccessToken("access-token-value"); + tokenDto.setRefreshToken("refresh-token-value"); + tokenDto.setExpiresIn(3600); + + return tokenDto; + } +} \ No newline at end of file diff --git a/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java b/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java new file mode 100644 index 0000000..b28eee9 --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/service/TermServiceTest.java @@ -0,0 +1,173 @@ +package com.stcom.smartmealtable.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stcom.smartmealtable.domain.member.Member; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.domain.term.TermAgreement; +import com.stcom.smartmealtable.repository.MemberRepository; +import com.stcom.smartmealtable.repository.TermAgreementRepository; +import com.stcom.smartmealtable.repository.TermRepository; +import com.stcom.smartmealtable.service.dto.TermAgreementRequestDto; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TermServiceTest { + + @InjectMocks + private TermService termService; + + @Mock + private TermRepository termRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private TermAgreementRepository termAgreementRepository; + + @Test + @DisplayName("모든 약관을 조회할 수 있다") + void findAll() { + // given + Term term1 = createTerm(1L, "이용약관", true); + Term term2 = createTerm(2L, "개인정보 처리방침", true); + Term term3 = createTerm(3L, "마케팅 정보 수신 동의", false); + + when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2, term3)); + + // when + List terms = termService.findAll(); + + // then + assertThat(terms).hasSize(3); + assertThat(terms.get(0).getTitle()).isEqualTo("이용약관"); + assertThat(terms.get(1).getTitle()).isEqualTo("개인정보 처리방침"); + assertThat(terms.get(2).getTitle()).isEqualTo("마케팅 정보 수신 동의"); + } + + @Test + @DisplayName("회원이 약관에 동의할 수 있다") + void agreeTerms() { + // given + Long memberId = 1L; + Member member = createMember(memberId); + + Term term1 = createTerm(1L, "이용약관", true); + Term term2 = createTerm(2L, "개인정보 처리방침", true); + Term term3 = createTerm(3L, "마케팅 정보 수신 동의", false); + + TermAgreementRequestDto dto1 = new TermAgreementRequestDto(1L, true); + TermAgreementRequestDto dto2 = new TermAgreementRequestDto(2L, true); + TermAgreementRequestDto dto3 = new TermAgreementRequestDto(3L, false); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2, term3)); + when(termRepository.findById(1L)).thenReturn(Optional.of(term1)); + when(termRepository.findById(2L)).thenReturn(Optional.of(term2)); + when(termRepository.findById(3L)).thenReturn(Optional.of(term3)); + + // when + termService.agreeTerms(memberId, Arrays.asList(dto1, dto2, dto3)); + + // then + verify(termAgreementRepository, times(3)).save(any(TermAgreement.class)); + } + + @Test + @DisplayName("필수 약관에 동의하지 않으면 예외가 발생한다") + void agreeTerms_RequiredTermNotAgreed() { + // given + Long memberId = 1L; + Member member = createMember(memberId); + + Term term1 = createTerm(1L, "이용약관", true); + Term term2 = createTerm(2L, "개인정보 처리방침", true); + + TermAgreementRequestDto dto1 = new TermAgreementRequestDto(1L, true); + TermAgreementRequestDto dto2 = new TermAgreementRequestDto(2L, false); // 필수 약관이지만 동의하지 않음 + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(termRepository.findAll()).thenReturn(Arrays.asList(term1, term2)); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(memberId, Arrays.asList(dto1, dto2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("필수 약관에 동의해야 합니다"); + } + + @Test + @DisplayName("존재하지 않는 회원 ID로 약관 동의를 시도하면 예외가 발생한다") + void agreeTerms_MemberNotFound() { + // given + Long nonExistentMemberId = 999L; + TermAgreementRequestDto dto = new TermAgreementRequestDto(1L, true); + + when(memberRepository.findById(nonExistentMemberId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(nonExistentMemberId, List.of(dto))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("존재하지 않는 회원입니다"); + } + + @Test + @DisplayName("존재하지 않는 약관 ID로 약관 동의를 시도하면 예외가 발생한다") + void agreeTerms_TermNotFound() { + // given + Long memberId = 1L; + Long nonExistentTermId = 999L; + Member member = createMember(memberId); + TermAgreementRequestDto dto = new TermAgreementRequestDto(nonExistentTermId, true); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(termRepository.findAll()).thenReturn(List.of()); + when(termRepository.findById(nonExistentTermId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> termService.agreeTerms(memberId, List.of(dto))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 약관입니다"); + } + + // 테스트용 회원 생성 헬퍼 메소드 + private Member createMember(Long id) { + Member member = new Member(); + return member; + } + + // 테스트용 약관 생성 헬퍼 메소드 + private Term createTerm(Long id, String title, Boolean isRequired) { + Term term = new Term(); + // 리플렉션을 통해 private 필드에 값 설정 + try { + java.lang.reflect.Field idField = Term.class.getDeclaredField("id"); + idField.setAccessible(true); + idField.set(term, id); + + java.lang.reflect.Field titleField = Term.class.getDeclaredField("title"); + titleField.setAccessible(true); + titleField.set(term, title); + + java.lang.reflect.Field isRequiredField = Term.class.getDeclaredField("isRequired"); + isRequiredField.setAccessible(true); + isRequiredField.set(term, isRequired); + } catch (Exception e) { + throw new RuntimeException(e); + } + return term; + } +} \ No newline at end of file From 88eb7de03336d7805c7fe885e796a005f15b464d Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:55:07 +0900 Subject: [PATCH 117/120] =?UTF-8?q?test:=20spring=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20Argument=20Resolver=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/argumentresolver/UserContextArgumentResolverTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java b/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java index 5b3a7db..4278906 100644 --- a/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java +++ b/src/test/java/com/stcom/smartmealtable/web/argumentresolver/UserContextArgumentResolverTest.java @@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.lenient; import com.stcom.smartmealtable.security.JwtTokenService; import com.stcom.smartmealtable.service.dto.MemberDto; @@ -40,7 +41,7 @@ class UserContextArgumentResolverTest { @BeforeEach void setUp() { - when(webRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(httpServletRequest); + lenient().when(webRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(httpServletRequest); } @Test @@ -62,7 +63,6 @@ void supportsParameter() { void doesNotSupportParameterWithoutAnnotation() { // given when(methodParameter.hasParameterAnnotation(UserContext.class)).thenReturn(false); - when(methodParameter.getParameterType()).thenReturn((Class) MemberDto.class); // when boolean result = resolver.supportsParameter(methodParameter); From f88fd875b2bc536a8f632407021ab2b8006220d6 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:55:24 +0900 Subject: [PATCH 118/120] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/MemberIntegrationTest.java | 2 +- .../repository/MemberRepositoryTest.java | 2 + .../web/controller/MemberControllerTest.java | 148 ++++++++++++------ 3 files changed, 101 insertions(+), 51 deletions(-) diff --git a/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java b/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java index 6b99bc2..30685a0 100644 --- a/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java +++ b/src/test/java/com/stcom/smartmealtable/integration/MemberIntegrationTest.java @@ -54,7 +54,7 @@ void createMember() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value("SUCCESS")) .andExpect(jsonPath("$.data.accessToken").isNotEmpty()) .andExpect(jsonPath("$.data.refreshToken").isNotEmpty()) .andExpect(jsonPath("$.data.newUser").value(true)); diff --git a/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java b/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java index 149bc27..fb0c9d8 100644 --- a/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java +++ b/src/test/java/com/stcom/smartmealtable/repository/MemberRepositoryTest.java @@ -10,8 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; @DataJpaTest +@ActiveProfiles("test") class MemberRepositoryTest { @Autowired diff --git a/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java b/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java index 8ef5ac9..d6f2365 100644 --- a/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java +++ b/src/test/java/com/stcom/smartmealtable/web/controller/MemberControllerTest.java @@ -4,11 +4,13 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -20,26 +22,38 @@ import com.stcom.smartmealtable.service.TermService; import com.stcom.smartmealtable.service.dto.MemberDto; import com.stcom.smartmealtable.web.argumentresolver.UserContext; +import com.stcom.smartmealtable.web.argumentresolver.UserContextArgumentResolver; import com.stcom.smartmealtable.web.controller.MemberController.CreateMemberRequest; import com.stcom.smartmealtable.web.controller.MemberController.EditMemberRequest; import com.stcom.smartmealtable.web.controller.MemberController.TermAgreementDto; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.FilterType; import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.bind.annotation.RestController; - -@WebMvcTest(value = MemberController.class, - includeFilters = { - @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {RestController.class}) - }) +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import io.jsonwebtoken.Claims; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(properties = { + "spring.main.allow-bean-definition-overriding=true" +}) class MemberControllerTest { @Autowired @@ -56,6 +70,24 @@ class MemberControllerTest { @MockBean private TermService termService; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + void setup() { + // 테스트용 MockMvc 설정 + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .build(); + + // UserContext 어노테이션 처리를 위한 설정 + // Claims 모킹 설정 + Claims claims = Mockito.mock(Claims.class); + when(claims.get("memberId", String.class)).thenReturn("1"); + when(jwtTokenService.extractClaims(anyString())).thenReturn(claims); + } @Test @DisplayName("이메일 중복 확인 API가 정상적으로 동작해야 한다") @@ -67,10 +99,11 @@ void checkEmail() throws Exception { // when & then mockMvc.perform(get("/api/v1/members/email/check") .param("email", email)) + .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.status").value("SUCCESS")); } - + @Test @DisplayName("회원 생성 API가 정상적으로 동작해야 한다") void createMember() throws Exception { @@ -78,27 +111,48 @@ void createMember() throws Exception { CreateMemberRequest request = new CreateMemberRequest( "test@example.com", "Password123!", "Password123!", "홍길동"); - JwtTokenResponseDto tokenDto = new JwtTokenResponseDto( - "test-access-token", - "test-refresh-token", - 3600, - "Bearer" - ); - tokenDto.setNewUser(true); + // Member 모킹 + Member mockMember = mock(Member.class); + when(mockMember.getId()).thenReturn(1L); - when(jwtTokenService.createTokenDto(anyLong(), any())).thenReturn(tokenDto); - doNothing().when(memberService).validateDuplicatedEmail(anyString()); - doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); - - // when & then - mockMvc.perform(post("/api/v1/members") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.accessToken").value("test-access-token")) - .andExpect(jsonPath("$.data.refreshToken").value("test-refresh-token")) - .andExpect(jsonPath("$.data.newUser").value(true)); + // Member Builder 모킹 + Member.MemberBuilder mockBuilder = mock(Member.MemberBuilder.class); + when(mockBuilder.fullName(anyString())).thenReturn(mockBuilder); + when(mockBuilder.email(anyString())).thenReturn(mockBuilder); + when(mockBuilder.rawPassword(anyString())).thenReturn(mockBuilder); + when(mockBuilder.build()).thenReturn(mockMember); + + // Member 정적 메소드 모킹 + try (var mockStatic = Mockito.mockStatic(Member.class)) { + mockStatic.when(Member::builder).thenReturn(mockBuilder); + + JwtTokenResponseDto tokenDto = new JwtTokenResponseDto( + "test-access-token", + "test-refresh-token", + 3600, + "Bearer" + ); + tokenDto.setNewUser(true); + + // MemberService 모킹 + doNothing().when(memberService).validateDuplicatedEmail(anyString()); + doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); + doNothing().when(memberService).saveMember(any(Member.class)); + + // JwtTokenService 모킹 + when(jwtTokenService.createTokenDto(anyLong(), any())).thenReturn(tokenDto); + + // when & then + mockMvc.perform(post("/api/v1/members") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.data.accessToken").value("test-access-token")) + .andExpect(jsonPath("$.data.refreshToken").value("test-refresh-token")) + .andExpect(jsonPath("$.data.newUser").value(true)); + } } @Test @@ -107,35 +161,32 @@ void editMember() throws Exception { // given EditMemberRequest request = new EditMemberRequest( "OldPassword123!", "NewPassword123!", "NewPassword123!"); - - MemberDto memberDto = MemberDto.builder() - .memberId(1L) - .build(); - + + doNothing().when(memberService).checkPasswordDoubly(anyString(), anyString()); + doNothing().when(memberService).changePassword(anyLong(), anyString(), anyString()); + // when & then mockMvc.perform(patch("/api/v1/members/me") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .header("Authorization", "Bearer test-token")) + .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.status").value("SUCCESS")); } @Test @DisplayName("회원 탈퇴 API가 정상적으로 동작해야 한다") void deleteMember() throws Exception { // given - MemberDto memberDto = MemberDto.builder() - .memberId(1L) - .build(); - doNothing().when(memberService).deleteByMemberId(anyLong()); // when & then mockMvc.perform(delete("/api/v1/members/me") - .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .header("Authorization", "Bearer test-token")) + .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.status").value("SUCCESS")); } @Test @@ -147,18 +198,15 @@ void signUpWithTermAgreement() throws Exception { new TermAgreementDto(2L, false) ); - MemberDto memberDto = MemberDto.builder() - .memberId(1L) - .build(); - doNothing().when(termService).agreeTerms(anyLong(), any()); // when & then mockMvc.perform(post("/api/v1/members/signup") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(agreements)) - .requestAttr("memberDto", memberDto)) // UserContext를 모킹 + .header("Authorization", "Bearer test-token")) + .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.success").value(true)); + .andExpect(jsonPath("$.status").value("SUCCESS")); } } \ No newline at end of file From 1806c668630708232552a44cc95fedec4b8c7b7f Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:55:37 +0900 Subject: [PATCH 119/120] =?UTF-8?q?test:=20=EC=9D=B4=EC=9A=A9=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=20=EC=A1=B0=ED=9A=8C=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/TermControllerTest.java | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/web/controller/TermControllerTest.java diff --git a/src/test/java/com/stcom/smartmealtable/web/controller/TermControllerTest.java b/src/test/java/com/stcom/smartmealtable/web/controller/TermControllerTest.java new file mode 100644 index 0000000..687a58a --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/web/controller/TermControllerTest.java @@ -0,0 +1,131 @@ +package com.stcom.smartmealtable.web.controller; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stcom.smartmealtable.domain.term.Term; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.TermService; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import io.jsonwebtoken.Claims; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(properties = { + "spring.main.allow-bean-definition-overriding=true" +}) +class TermControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private TermService termService; + + @MockBean + private JwtTokenService jwtTokenService; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + void setup() { + // 테스트용 MockMvc 설정 + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .build(); + + // UserContext 어노테이션 처리를 위한 설정 + // Claims 모킹 설정 + Claims claims = Mockito.mock(Claims.class); + when(claims.get("memberId", String.class)).thenReturn("1"); + when(jwtTokenService.extractClaims(Mockito.anyString())).thenReturn(claims); + } + + @Test + @DisplayName("모든 약관을 조회할 수 있다") + void getTerms() throws Exception { + // given + Term term1 = createTerm(1L, "이용약관", "이용약관 내용입니다.", true); + Term term2 = createTerm(2L, "개인정보 처리방침", "개인정보 처리방침 내용입니다.", true); + Term term3 = createTerm(3L, "마케팅 정보 수신 동의", "마케팅 정보 수신 동의 내용입니다.", false); + + List terms = Arrays.asList(term1, term2, term3); + + when(termService.findAll()).thenReturn(terms); + + // when & then + mockMvc.perform(get("/api/v1/terms") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(3)) + .andExpect(jsonPath("$.data[0].termId").value(1)) + .andExpect(jsonPath("$.data[0].title").value("이용약관")) + .andExpect(jsonPath("$.data[0].content").value("이용약관 내용입니다.")) + .andExpect(jsonPath("$.data[0].required").value(true)) + .andExpect(jsonPath("$.data[1].termId").value(2)) + .andExpect(jsonPath("$.data[1].title").value("개인정보 처리방침")) + .andExpect(jsonPath("$.data[1].content").value("개인정보 처리방침 내용입니다.")) + .andExpect(jsonPath("$.data[1].required").value(true)) + .andExpect(jsonPath("$.data[2].termId").value(3)) + .andExpect(jsonPath("$.data[2].title").value("마케팅 정보 수신 동의")) + .andExpect(jsonPath("$.data[2].content").value("마케팅 정보 수신 동의 내용입니다.")) + .andExpect(jsonPath("$.data[2].required").value(false)); + } + + @Test + @DisplayName("약관이 없는 경우 빈 배열을 반환한다") + void getTerms_Empty() throws Exception { + // given + when(termService.findAll()).thenReturn(List.of()); + + // when & then + mockMvc.perform(get("/api/v1/terms") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + } + + // 테스트용 약관 생성 헬퍼 메소드 + private Term createTerm(Long id, String title, String content, Boolean isRequired) { + Term term = mock(Term.class); + when(term.getId()).thenReturn(id); + when(term.getTitle()).thenReturn(title); + when(term.getContent()).thenReturn(content); + when(term.getIsRequired()).thenReturn(isRequired); + return term; + } +} \ No newline at end of file From 250d6f4bb41a0bc33395d785efbcea774f475b69 Mon Sep 17 00:00:00 2001 From: Junseo Bae Date: Fri, 23 May 2025 04:55:55 +0900 Subject: [PATCH 120/120] =?UTF-8?q?test:=20=EA=B7=B8=EB=A3=B9=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/GroupControllerTest.java | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/test/java/com/stcom/smartmealtable/web/controller/GroupControllerTest.java diff --git a/src/test/java/com/stcom/smartmealtable/web/controller/GroupControllerTest.java b/src/test/java/com/stcom/smartmealtable/web/controller/GroupControllerTest.java new file mode 100644 index 0000000..a5e777f --- /dev/null +++ b/src/test/java/com/stcom/smartmealtable/web/controller/GroupControllerTest.java @@ -0,0 +1,169 @@ +package com.stcom.smartmealtable.web.controller; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stcom.smartmealtable.domain.Address.Address; +import com.stcom.smartmealtable.domain.group.CompanyGroup; +import com.stcom.smartmealtable.domain.group.Group; +import com.stcom.smartmealtable.domain.group.IndustryType; +import com.stcom.smartmealtable.domain.group.SchoolGroup; +import com.stcom.smartmealtable.domain.group.SchoolType; +import com.stcom.smartmealtable.security.JwtTokenService; +import com.stcom.smartmealtable.service.GroupService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import io.jsonwebtoken.Claims; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(properties = { + "spring.main.allow-bean-definition-overriding=true" +}) +class GroupControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private GroupService groupService; + + @MockBean + private JwtTokenService jwtTokenService; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + void setup() { + // 테스트용 MockMvc 설정 + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .build(); + + // UserContext 어노테이션 처리를 위한 설정 + // Claims 모킹 설정 + Claims claims = Mockito.mock(Claims.class); + when(claims.get("memberId", String.class)).thenReturn("1"); + when(jwtTokenService.extractClaims(Mockito.anyString())).thenReturn(claims); + } + + @Test + @DisplayName("키워드로 그룹을 검색할 수 있다") + void searchGroup() throws Exception { + // given + String keyword = "테스트"; + + // 테스트용 그룹 생성 + CompanyGroup companyGroup = createCompanyGroup("테스트 회사", IndustryType.IT, + createAddress("서울시 강남구 테헤란로 123")); + + SchoolGroup schoolGroup = createSchoolGroup("테스트 학교", SchoolType.UNIVERSITY_FOUR_YEAR, + createAddress("서울시 서초구 방배로 456")); + + when(groupService.findGroupsByKeyword(keyword)) + .thenReturn(List.of(companyGroup, schoolGroup)); + + // when & then + mockMvc.perform(get("/api/v1/groups") + .param("keyword", keyword) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(2)) + .andExpect(jsonPath("$.data[0].name").value("테스트 회사")) + .andExpect(jsonPath("$.data[0].groupType").value("IT")) + .andExpect(jsonPath("$.data[0].roadAddress").value("서울시 강남구 테헤란로 123")) + .andExpect(jsonPath("$.data[1].name").value("테스트 학교")) + .andExpect(jsonPath("$.data[1].groupType").value("UNIVERSITY_FOUR_YEAR")) + .andExpect(jsonPath("$.data[1].roadAddress").value("서울시 서초구 방배로 456")); + } + + @Test + @DisplayName("빈 키워드로 그룹 검색시 에러가 발생한다") + void searchGroup_EmptyKeyword() throws Exception { + // given + String keyword = ""; + + // when & then + mockMvc.perform(get("/api/v1/groups") + .param("keyword", keyword) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("ERROR")) + .andExpect(jsonPath("$.message").value("키워드가 비어있습니다. 키워드를 입력해주세요")); + } + + @Test + @DisplayName("키워드로 그룹 검색시 결과가 없으면 빈 리스트를 반환한다") + void searchGroup_NoResults() throws Exception { + // given + String keyword = "존재하지 않는 키워드"; + + when(groupService.findGroupsByKeyword(keyword)) + .thenReturn(List.of()); + + // when & then + mockMvc.perform(get("/api/v1/groups") + .param("keyword", keyword) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data.length()").value(0)); + } + + // 테스트용 주소 생성 헬퍼 메소드 + private Address createAddress(String roadAddress) { + Address address = mock(Address.class); + when(address.getRoadAddress()).thenReturn(roadAddress); + return address; + } + + // 테스트용 회사 그룹 생성 헬퍼 메소드 + private CompanyGroup createCompanyGroup(String name, IndustryType industryType, Address address) { + CompanyGroup group = mock(CompanyGroup.class); + when(group.getName()).thenReturn(name); + when(group.getTypeName()).thenReturn(industryType.getDescription()); + when(group.getAddress()).thenReturn(address); + return group; + } + + // 테스트용 학교 그룹 생성 헬퍼 메소드 + private SchoolGroup createSchoolGroup(String name, SchoolType schoolType, Address address) { + SchoolGroup group = mock(SchoolGroup.class); + when(group.getName()).thenReturn(name); + when(group.getTypeName()).thenReturn(schoolType.name()); + when(group.getAddress()).thenReturn(address); + return group; + } +} \ No newline at end of file