From 00ec5453202e61730da3c78168a0c21360232ead Mon Sep 17 00:00:00 2001 From: 1Seob Date: Thu, 28 May 2026 21:52:37 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=ED=99=88=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=EC=97=90=20=ED=94=8C=EB=9E=AB=ED=8F=BC=EB=B3=84=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EB=B2=84=EC=A0=84=20=EC=A0=95=EB=B3=B4=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 --- .../application/AppVersionQueryService.java | 35 +++++++++++++++++++ .../appversion/core/entity/AppPlatform.java | 7 ++++ .../appversion/core/entity/AppVersion.java | 30 ++++++++++++++++ .../core/repository/AppVersionRepository.java | 7 ++++ .../response/HomeLatestVersionResponse.java | 16 +++++++++ .../api/dto/response/HomeResponse.java | 7 ++-- .../application/HomeQueryService.java | 10 ++++-- ...527_1600__IS_create_app_versions_table.sql | 13 +++++++ 8 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java create mode 100644 src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java new file mode 100644 index 00000000..6d50be64 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -0,0 +1,35 @@ +package com.devkor.ifive.nadab.domain.appversion.application; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeLatestVersionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AppVersionQueryService { + + private final AppVersionRepository appVersionRepository; + + public HomeLatestVersionResponse getHomeLatestVersion() { + Map latestVersionByPlatform = appVersionRepository.findAll().stream() + .collect(Collectors.toMap( + AppVersion::getPlatform, + AppVersion::getLatestVersion, + (left, right) -> right + )); + + return new HomeLatestVersionResponse( + latestVersionByPlatform.get(AppPlatform.IOS), + latestVersionByPlatform.get(AppPlatform.ANDROID), + latestVersionByPlatform.get(AppPlatform.WEB) + ); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java new file mode 100644 index 00000000..f830f5c9 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java @@ -0,0 +1,7 @@ +package com.devkor.ifive.nadab.domain.appversion.core.entity; + +public enum AppPlatform { + IOS, + ANDROID, + WEB +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java new file mode 100644 index 00000000..60c26295 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -0,0 +1,30 @@ +package com.devkor.ifive.nadab.domain.appversion.core.entity; + +import com.devkor.ifive.nadab.global.shared.entity.AuditableEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "app_versions", + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_versions_platform", columnNames = {"platform"}) + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AppVersion extends AuditableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "platform", nullable = false, length = 20) + private AppPlatform platform; + + @Column(name = "latest_version", nullable = false, length = 30) + private String latestVersion; +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java new file mode 100644 index 00000000..c325199b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java @@ -0,0 +1,7 @@ +package com.devkor.ifive.nadab.domain.appversion.core.repository; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AppVersionRepository extends JpaRepository { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java new file mode 100644 index 00000000..63fbaa5a --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java @@ -0,0 +1,16 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "플랫폼별 최신 앱 버전") +public record HomeLatestVersionResponse( + @Schema(description = "iOS 최신 앱 버전", example = "1.2.0", nullable = true) + String ios, + + @Schema(description = "Android 최신 앱 버전", example = "1.2.0", nullable = true) + String android, + + @Schema(description = "Web 최신 버전", example = "1.2.0", nullable = true) + String web +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java index 3c59b143..4f75b7e9 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeResponse.java @@ -20,6 +20,9 @@ public record HomeResponse( List answeredFriendProfiles, @Schema(description = "나와 같은 오늘의 질문에 답변한 친구의 총 수", example = "8") - int answeredFriendCount + int answeredFriendCount, + + @Schema(description = "플랫폼별 최신 앱 버전") + HomeLatestVersionResponse latestVersion ) { -} \ No newline at end of file +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java index a1d05f01..ed98e8b3 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java @@ -1,5 +1,7 @@ package com.devkor.ifive.nadab.domain.dailyreport.application; +import com.devkor.ifive.nadab.domain.appversion.application.AppVersionQueryService; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeLatestVersionResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeResponse; import com.devkor.ifive.nadab.domain.dailyreport.core.repository.AnswerEntryQueryRepository; import com.devkor.ifive.nadab.domain.question.core.entity.UserDailyQuestion; @@ -27,6 +29,7 @@ public class HomeQueryService { private final AnswerEntryQueryRepository answerEntryQueryRepository; private final UserDailyQuestionRepository userDailyQuestionRepository; private final ProfileImageUrlBuilder profileImageUrlBuilder; + private final AppVersionQueryService appVersionQueryService; public HomeResponse getHomeData(Long userId) { LocalDate today = TodayDateTimeProvider.getTodayDate(); @@ -71,12 +74,15 @@ public HomeResponse getHomeData(Long userId) { } // 7. 응답 생성 + HomeLatestVersionResponse latestVersion = appVersionQueryService.getHomeLatestVersion(); + return new HomeResponse( weeklyAnsweredDates, currentStreak, totalAnsweredDays, answeredFriendProfiles, - answeredFriendCount + answeredFriendCount, + latestVersion ); } @@ -105,4 +111,4 @@ private int calculateCurrentStreak(LocalDate today, List answerDates) return streak; } -} \ No newline at end of file +} diff --git a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql new file mode 100644 index 00000000..8e51e6f5 --- /dev/null +++ b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE app_versions ( + id BIGSERIAL PRIMARY KEY, + platform VARCHAR(20) NOT NULL, + latest_version VARCHAR(30) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uk_app_versions_platform UNIQUE (platform) +); + +INSERT INTO app_versions (platform, latest_version) VALUES + ('IOS', '1.2.0'), + ('ANDROID', '1.2.0'), + ('WEB', '1.2.0'); From d0cbb95fd92933b7f1665faff128b9c0f66f1306 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sat, 30 May 2026 21:47:08 +0900 Subject: [PATCH 02/10] =?UTF-8?q?refactor:=20app=5Fversions=EB=A5=BC=20?= =?UTF-8?q?=EB=88=84=EC=A0=81=20=EC=9D=B4=EB=A0=A5=ED=98=95=20=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=EB=A7=88=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app_versions 테이블 컬럼을 latest_version에서 version으로 변경, is_latest(boolean) 컬럼 추가, AppVersion 엔티티를 version/isLatest 필드 구조로 변경 등 --- .../application/AppVersionQueryService.java | 4 ++-- .../appversion/core/entity/AppVersion.java | 9 ++++++--- .../core/repository/AppVersionRepository.java | 3 +++ ...60527_1600__IS_create_app_versions_table.sql | 17 +++++++++++------ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java index 6d50be64..44f00eec 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -19,10 +19,10 @@ public class AppVersionQueryService { private final AppVersionRepository appVersionRepository; public HomeLatestVersionResponse getHomeLatestVersion() { - Map latestVersionByPlatform = appVersionRepository.findAll().stream() + Map latestVersionByPlatform = appVersionRepository.findByIsLatestTrue().stream() .collect(Collectors.toMap( AppVersion::getPlatform, - AppVersion::getLatestVersion, + AppVersion::getVersion, (left, right) -> right )); diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java index 60c26295..cc94058d 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -10,7 +10,7 @@ @Table( name = "app_versions", uniqueConstraints = { - @UniqueConstraint(name = "uk_app_versions_platform", columnNames = {"platform"}) + @UniqueConstraint(name = "uk_app_versions_platform_version", columnNames = {"platform", "version"}) } ) @Getter @@ -25,6 +25,9 @@ public class AppVersion extends AuditableEntity { @Column(name = "platform", nullable = false, length = 20) private AppPlatform platform; - @Column(name = "latest_version", nullable = false, length = 30) - private String latestVersion; + @Column(name = "version", nullable = false, length = 30) + private String version; + + @Column(name = "is_latest", nullable = false) + private Boolean isLatest; } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java index c325199b..c9f7ba80 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java @@ -3,5 +3,8 @@ import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface AppVersionRepository extends JpaRepository { + List findByIsLatestTrue(); } diff --git a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql index 8e51e6f5..cccef7d1 100644 --- a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql +++ b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql @@ -1,13 +1,18 @@ CREATE TABLE app_versions ( id BIGSERIAL PRIMARY KEY, platform VARCHAR(20) NOT NULL, - latest_version VARCHAR(30) NOT NULL, + version VARCHAR(30) NOT NULL, + is_latest BOOLEAN NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT uk_app_versions_platform UNIQUE (platform) + CONSTRAINT uk_app_versions_platform_version UNIQUE (platform, version) ); -INSERT INTO app_versions (platform, latest_version) VALUES - ('IOS', '1.2.0'), - ('ANDROID', '1.2.0'), - ('WEB', '1.2.0'); +CREATE UNIQUE INDEX uk_app_versions_platform_latest + ON app_versions (platform) + WHERE is_latest = true; + +INSERT INTO app_versions (platform, version, is_latest) VALUES + ('IOS', '1.2.0', true), + ('ANDROID', '1.2.0', true), + ('WEB', '1.2.0', true); From 345d609e9a04f11af3a3ec3d9fc6019cb0a1bff7 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sat, 30 May 2026 23:04:11 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20app=5Fversions=EC=97=90=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0(summary/items)=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=99=88=20=EC=9D=91=EB=8B=B5=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app_versions 스키마에 summary 컬럼과 items(jsonb) 컬럼 추가, AppVersion 엔티티에 summary, items 필드 추가 및 jsonb 매핑 적용, 홈 응답의 latestVersion 구조를 플랫폼별 상세 객체로 확장 --- .../application/AppVersionQueryService.java | 22 +++++++++++++++++-- .../appversion/core/entity/AppVersion.java | 11 ++++++++++ .../core/entity/AppVersionItem.java | 7 ++++++ .../response/HomeLatestVersionResponse.java | 9 +++----- .../response/HomePlatformVersionResponse.java | 18 +++++++++++++++ .../dto/response/HomeVersionItemResponse.java | 13 +++++++++++ ...527_1600__IS_create_app_versions_table.sql | 10 +++++---- 7 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeVersionItemResponse.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java index 44f00eec..58577b44 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -4,11 +4,15 @@ import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeLatestVersionResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomePlatformVersionResponse; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeVersionItemResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -19,10 +23,10 @@ public class AppVersionQueryService { private final AppVersionRepository appVersionRepository; public HomeLatestVersionResponse getHomeLatestVersion() { - Map latestVersionByPlatform = appVersionRepository.findByIsLatestTrue().stream() + Map latestVersionByPlatform = appVersionRepository.findByIsLatestTrue().stream() .collect(Collectors.toMap( AppVersion::getPlatform, - AppVersion::getVersion, + this::toPlatformResponse, (left, right) -> right )); @@ -32,4 +36,18 @@ public HomeLatestVersionResponse getHomeLatestVersion() { latestVersionByPlatform.get(AppPlatform.WEB) ); } + + private HomePlatformVersionResponse toPlatformResponse(AppVersion appVersion) { + List items = Optional.ofNullable(appVersion.getItems()) + .orElse(List.of()) + .stream() + .map(item -> new HomeVersionItemResponse(item.title(), item.description())) + .toList(); + + return new HomePlatformVersionResponse( + appVersion.getVersion(), + appVersion.getSummary(), + items + ); + } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java index cc94058d..3daec1e5 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -1,10 +1,14 @@ package com.devkor.ifive.nadab.domain.appversion.core.entity; import com.devkor.ifive.nadab.global.shared.entity.AuditableEntity; +import io.hypersistence.utils.hibernate.type.json.JsonType; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; + +import java.util.List; @Entity @Table( @@ -30,4 +34,11 @@ public class AppVersion extends AuditableEntity { @Column(name = "is_latest", nullable = false) private Boolean isLatest; + + @Column(name = "summary", nullable = false, length = 120) + private String summary; + + @Type(JsonType.class) + @Column(name = "items", columnDefinition = "jsonb", nullable = false) + private List items; } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java new file mode 100644 index 00000000..0f4921ef --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java @@ -0,0 +1,7 @@ +package com.devkor.ifive.nadab.domain.appversion.core.entity; + +public record AppVersionItem( + String title, + String description +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java index 63fbaa5a..a0d488be 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java @@ -4,13 +4,10 @@ @Schema(description = "플랫폼별 최신 앱 버전") public record HomeLatestVersionResponse( - @Schema(description = "iOS 최신 앱 버전", example = "1.2.0", nullable = true) - String ios, + HomePlatformVersionResponse ios, - @Schema(description = "Android 최신 앱 버전", example = "1.2.0", nullable = true) - String android, + HomePlatformVersionResponse android, - @Schema(description = "Web 최신 버전", example = "1.2.0", nullable = true) - String web + HomePlatformVersionResponse web ) { } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java new file mode 100644 index 00000000..ed53ab6e --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java @@ -0,0 +1,18 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "플랫폼별 최신 버전 정보") +public record HomePlatformVersionResponse( + @Schema(description = "최신 앱 버전", example = "1.2.0") + String version, + + @Schema(description = "업데이트 요약 문장", example = "좋아요와 댓글로 마음을 전해요.") + String summary, + + @Schema(description = "업데이트 항목 목록") + List items +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeVersionItemResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeVersionItemResponse.java new file mode 100644 index 00000000..cf06f421 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeVersionItemResponse.java @@ -0,0 +1,13 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "업데이트 항목") +public record HomeVersionItemResponse( + @Schema(description = "업데이트 항목명", example = "월간 리포트") + String title, + + @Schema(description = "업데이트 상세 설명", example = "한 달의 기록을 한눈에 돌아볼 수 있어요.") + String description +) { +} diff --git a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql index cccef7d1..0557cc60 100644 --- a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql +++ b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql @@ -3,6 +3,8 @@ CREATE TABLE app_versions ( platform VARCHAR(20) NOT NULL, version VARCHAR(30) NOT NULL, is_latest BOOLEAN NOT NULL, + summary VARCHAR(120) NOT NULL, + items JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uk_app_versions_platform_version UNIQUE (platform, version) @@ -12,7 +14,7 @@ CREATE UNIQUE INDEX uk_app_versions_platform_latest ON app_versions (platform) WHERE is_latest = true; -INSERT INTO app_versions (platform, version, is_latest) VALUES - ('IOS', '1.2.0', true), - ('ANDROID', '1.2.0', true), - ('WEB', '1.2.0', true); +INSERT INTO app_versions (platform, version, is_latest, summary, items) VALUES + ('IOS', '1.2.0', true, '', '[]'::jsonb), + ('ANDROID', '1.2.0', true, '', '[]'::jsonb), + ('WEB', '1.2.0', true, '', '[]'::jsonb); From d0909503d312a3e18566dae83a7006b68e84bcc2 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 02:59:11 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=ED=99=88=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B2=84=EC=A0=84=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=8B=A4=EC=8B=9C=20=EB=B3=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=99=88=20=EC=9D=91=EB=8B=B5=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user_app_version_dismissals 테이블 생성, UserAppVersionDismissal 엔티티 구현, POST /home/version-dismissals API 추가, HomePlatformVersionResponse에 appVersionId, dismissed 필드 추가 --- .../AppVersionDismissalCommandService.java | 42 +++++++++++++++++ .../application/AppVersionQueryService.java | 26 ++++++++-- .../core/entity/UserAppVersionDismissal.java | 47 +++++++++++++++++++ .../UserAppVersionDismissalRepository.java | 24 ++++++++++ .../dailyreport/api/HomeController.java | 44 ++++++++++++++++- .../request/HomeVersionDismissRequest.java | 12 +++++ .../response/HomePlatformVersionResponse.java | 8 +++- .../application/HomeQueryService.java | 2 +- .../nadab/global/core/response/ErrorCode.java | 6 ++- ...eate_user_app_version_dismissals_table.sql | 11 +++++ 10 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionDismissalCommandService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/UserAppVersionDismissal.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/UserAppVersionDismissalRepository.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/HomeVersionDismissRequest.java create mode 100644 src/main/resources/db/migration/V20260530_2300__IS_create_user_app_version_dismissals_table.sql diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionDismissalCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionDismissalCommandService.java new file mode 100644 index 00000000..2a8588aa --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionDismissalCommandService.java @@ -0,0 +1,42 @@ +package com.devkor.ifive.nadab.domain.appversion.application; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.entity.UserAppVersionDismissal; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import com.devkor.ifive.nadab.domain.appversion.core.repository.UserAppVersionDismissalRepository; +import com.devkor.ifive.nadab.domain.user.core.entity.User; +import com.devkor.ifive.nadab.domain.user.core.repository.UserRepository; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class AppVersionDismissalCommandService { + + private final UserRepository userRepository; + private final AppVersionRepository appVersionRepository; + private final UserAppVersionDismissalRepository userAppVersionDismissalRepository; + + public void dismiss(Long userId, Long appVersionId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); + + AppVersion appVersion = appVersionRepository.findById(appVersionId) + .orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_NOT_FOUND)); + + if (userAppVersionDismissalRepository.existsByUserIdAndAppVersionId(userId, appVersionId)) { + return; + } + + try { + userAppVersionDismissalRepository.save(UserAppVersionDismissal.create(user, appVersion)); + } catch (DataIntegrityViolationException ignored) { + // Ignore duplicate insert race condition. + } + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java index 58577b44..26419765 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -3,6 +3,7 @@ import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import com.devkor.ifive.nadab.domain.appversion.core.repository.UserAppVersionDismissalRepository; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeLatestVersionResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomePlatformVersionResponse; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeVersionItemResponse; @@ -13,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -21,12 +23,24 @@ public class AppVersionQueryService { private final AppVersionRepository appVersionRepository; + private final UserAppVersionDismissalRepository userAppVersionDismissalRepository; - public HomeLatestVersionResponse getHomeLatestVersion() { - Map latestVersionByPlatform = appVersionRepository.findByIsLatestTrue().stream() + public HomeLatestVersionResponse getHomeLatestVersion(Long userId) { + List latestVersions = appVersionRepository.findByIsLatestTrue(); + + List appVersionIds = latestVersions.stream() + .map(AppVersion::getId) + .toList(); + + Set dismissedAppVersionIds = appVersionIds.isEmpty() + ? Set.of() + : userAppVersionDismissalRepository.findDismissedAppVersionIds(userId, appVersionIds).stream() + .collect(Collectors.toSet()); + + Map latestVersionByPlatform = latestVersions.stream() .collect(Collectors.toMap( AppVersion::getPlatform, - this::toPlatformResponse, + appVersion -> toPlatformResponse(appVersion, dismissedAppVersionIds.contains(appVersion.getId())), (left, right) -> right )); @@ -37,7 +51,7 @@ public HomeLatestVersionResponse getHomeLatestVersion() { ); } - private HomePlatformVersionResponse toPlatformResponse(AppVersion appVersion) { + private HomePlatformVersionResponse toPlatformResponse(AppVersion appVersion, boolean dismissed) { List items = Optional.ofNullable(appVersion.getItems()) .orElse(List.of()) .stream() @@ -45,9 +59,11 @@ private HomePlatformVersionResponse toPlatformResponse(AppVersion appVersion) { .toList(); return new HomePlatformVersionResponse( + appVersion.getId(), appVersion.getVersion(), appVersion.getSummary(), - items + items, + dismissed ); } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/UserAppVersionDismissal.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/UserAppVersionDismissal.java new file mode 100644 index 00000000..277e3d43 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/UserAppVersionDismissal.java @@ -0,0 +1,47 @@ +package com.devkor.ifive.nadab.domain.appversion.core.entity; + +import com.devkor.ifive.nadab.domain.user.core.entity.User; +import com.devkor.ifive.nadab.global.shared.entity.CreatableEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "user_app_version_dismissals", + uniqueConstraints = { + @UniqueConstraint(name = "uk_uavd_user_app_version", columnNames = {"user_id", "app_version_id"}) + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class UserAppVersionDismissal extends CreatableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn( + name = "user_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_uavd_user") + ) + private User user; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn( + name = "app_version_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_uavd_app_version") + ) + private AppVersion appVersion; + + public static UserAppVersionDismissal create(User user, AppVersion appVersion) { + UserAppVersionDismissal dismissal = new UserAppVersionDismissal(); + dismissal.user = user; + dismissal.appVersion = appVersion; + return dismissal; + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/UserAppVersionDismissalRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/UserAppVersionDismissalRepository.java new file mode 100644 index 00000000..2a506975 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/UserAppVersionDismissalRepository.java @@ -0,0 +1,24 @@ +package com.devkor.ifive.nadab.domain.appversion.core.repository; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.UserAppVersionDismissal; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface UserAppVersionDismissalRepository extends JpaRepository { + + boolean existsByUserIdAndAppVersionId(Long userId, Long appVersionId); + + @Query(""" + select d.appVersion.id + from UserAppVersionDismissal d + where d.user.id = :userId + and d.appVersion.id in :appVersionIds + """) + List findDismissedAppVersionIds( + @Param("userId") Long userId, + @Param("appVersionIds") List appVersionIds + ); +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java index 89405f59..36918941 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java @@ -1,5 +1,7 @@ package com.devkor.ifive.nadab.domain.dailyreport.api; +import com.devkor.ifive.nadab.domain.appversion.application.AppVersionDismissalCommandService; +import com.devkor.ifive.nadab.domain.dailyreport.api.dto.request.HomeVersionDismissRequest; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeResponse; import com.devkor.ifive.nadab.domain.dailyreport.application.HomeQueryService; import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; @@ -11,11 +13,14 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; 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; @@ -26,6 +31,7 @@ public class HomeController { private final HomeQueryService homeQueryService; + private final AppVersionDismissalCommandService appVersionDismissalCommandService; @GetMapping @PreAuthorize("isAuthenticated()") @@ -40,6 +46,14 @@ public class HomeController { 3. 총 기록 일수: 실제 답변한 날짜의 총 개수 4. 친구 프로필: 나와 같은 오늘의 질문에 답변한 친구들의 프로필 사진 URL (최대 5개) 5. 친구 답변 수: 나와 같은 오늘의 질문에 답변한 친구의 총 수 + 6. 플랫폼별 최신 버전 정보 + - 앱 버전 ID + - 최신 버전 문자열 + - 업데이트 요약 문장 + - 업데이트 항목 목록 + - 업데이트 항목명 + - 업데이트 상세 설명 + - 다시 보지 않기 여부 ### 친구 프로필 조회 기준 - 나의 오늘의 질문과 동일한 질문에 답변한 친구만 표시 @@ -98,4 +112,32 @@ public ResponseEntity> getHomeData( HomeResponse response = homeQueryService.getHomeData(principal.getId()); return ApiResponseEntity.ok(response); } -} \ No newline at end of file + + @PostMapping("/version-dismissals") + @PreAuthorize("isAuthenticated()") + @Operation( + summary = "홈 업데이트 다시 보지 않기 저장", + description = """ + 사용자가 홈화면에 표시되는 앱 버전 업데이트 알림을 다시 보지 않도록 설정합니다. + + ### 요청 정보 + - appVersionId: 숨김 처리할 앱 버전 ID (홈화면 정보 조회 api에서 제공되는 앱 버전 ID 사용) + """, + security = @SecurityRequirement(name = "bearerAuth"), + responses = { + @ApiResponse(responseCode = "204", description = "저장 성공", content = @Content), + @ApiResponse(responseCode = "401", description = "인증 실패", content = @Content), + @ApiResponse(responseCode = "404", description = """ + - ErrorCode: USER_NOT_FOUND - 사용자를 찾을 수 없음 + - ErrorCode: APP_VERSION_NOT_FOUND - 앱 버전을 찾을 수 없음 + """, content = @Content) + } + ) + public ResponseEntity> dismissHomeVersion( + @AuthenticationPrincipal UserPrincipal principal, + @Valid @RequestBody HomeVersionDismissRequest request + ) { + appVersionDismissalCommandService.dismiss(principal.getId(), request.appVersionId()); + return ApiResponseEntity.noContent(); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/HomeVersionDismissRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/HomeVersionDismissRequest.java new file mode 100644 index 00000000..f1981bf4 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/request/HomeVersionDismissRequest.java @@ -0,0 +1,12 @@ +package com.devkor.ifive.nadab.domain.dailyreport.api.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "홈 버전 업데이트 다시 보지 않기 요청") +public record HomeVersionDismissRequest( + @NotNull + @Schema(description = "숨김 처리할 앱 버전 ID", example = "1") + Long appVersionId +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java index ed53ab6e..34653ac1 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomePlatformVersionResponse.java @@ -6,6 +6,9 @@ @Schema(description = "플랫폼별 최신 버전 정보") public record HomePlatformVersionResponse( + @Schema(description = "앱 버전 ID", example = "1") + Long appVersionId, + @Schema(description = "최신 앱 버전", example = "1.2.0") String version, @@ -13,6 +16,9 @@ public record HomePlatformVersionResponse( String summary, @Schema(description = "업데이트 항목 목록") - List items + List items, + + @Schema(description = "다시 보지 않기 여부", example = "false") + boolean dismissed ) { } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java index ed98e8b3..4f8324ae 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/application/HomeQueryService.java @@ -74,7 +74,7 @@ public HomeResponse getHomeData(Long userId) { } // 7. 응답 생성 - HomeLatestVersionResponse latestVersion = appVersionQueryService.getHomeLatestVersion(); + HomeLatestVersionResponse latestVersion = appVersionQueryService.getHomeLatestVersion(userId); return new HomeResponse( weeklyAnsweredDates, diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java index 3c40d991..b6e2d5a4 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java +++ b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java @@ -416,7 +416,11 @@ public enum ErrorCode { DAILY_REPORT_LIKE_LIST_FORBIDDEN(HttpStatus.FORBIDDEN, "본인의 게시글 좋아요 리스트만 조회할 수 있습니다"), // 404 Not Found - LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "좋아요를 찾을 수 없습니다"); + LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "좋아요를 찾을 수 없습니다"), + + // ==================== APP_VERSION (앱 버전) ==================== + // 404 Not Found + APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전을 찾을 수 없습니다"); private final HttpStatus httpStatus; diff --git a/src/main/resources/db/migration/V20260530_2300__IS_create_user_app_version_dismissals_table.sql b/src/main/resources/db/migration/V20260530_2300__IS_create_user_app_version_dismissals_table.sql new file mode 100644 index 00000000..bbf424e3 --- /dev/null +++ b/src/main/resources/db/migration/V20260530_2300__IS_create_user_app_version_dismissals_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE user_app_version_dismissals ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + app_version_id BIGINT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT fk_uavd_user FOREIGN KEY (user_id) + REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_uavd_app_version FOREIGN KEY (app_version_id) + REFERENCES app_versions(id) ON DELETE CASCADE, + CONSTRAINT uk_uavd_user_app_version UNIQUE (user_id, app_version_id) +); From 6d4e30c63409f4db7d1bda4167145441c0d4c1ff Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 19:11:27 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20=EC=95=B1=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5/=EC=A0=95=EC=B1=85=EC=97=90=EC=84=9C=20WEB=20?= =?UTF-8?q?=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppPlatform enum에서 WEB 제거 (IOS, ANDROID만 유지), HomeLatestVersionResponse에서 web 필드 제거 --- .../domain/appversion/application/AppVersionQueryService.java | 3 +-- .../nadab/domain/appversion/core/entity/AppPlatform.java | 3 +-- .../ifive/nadab/domain/dailyreport/api/HomeController.java | 2 +- .../api/dto/response/HomeLatestVersionResponse.java | 4 +--- .../V20260527_1600__IS_create_app_versions_table.sql | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java index 26419765..fadc9516 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -46,8 +46,7 @@ public HomeLatestVersionResponse getHomeLatestVersion(Long userId) { return new HomeLatestVersionResponse( latestVersionByPlatform.get(AppPlatform.IOS), - latestVersionByPlatform.get(AppPlatform.ANDROID), - latestVersionByPlatform.get(AppPlatform.WEB) + latestVersionByPlatform.get(AppPlatform.ANDROID) ); } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java index f830f5c9..a7198067 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppPlatform.java @@ -2,6 +2,5 @@ public enum AppPlatform { IOS, - ANDROID, - WEB + ANDROID } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java index 36918941..c32aa6ef 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/HomeController.java @@ -46,7 +46,7 @@ public class HomeController { 3. 총 기록 일수: 실제 답변한 날짜의 총 개수 4. 친구 프로필: 나와 같은 오늘의 질문에 답변한 친구들의 프로필 사진 URL (최대 5개) 5. 친구 답변 수: 나와 같은 오늘의 질문에 답변한 친구의 총 수 - 6. 플랫폼별 최신 버전 정보 + 6. 플랫폼별 최신 버전 정보 (android, ios) - 앱 버전 ID - 최신 버전 문자열 - 업데이트 요약 문장 diff --git a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java index a0d488be..b9f7de89 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/dailyreport/api/dto/response/HomeLatestVersionResponse.java @@ -6,8 +6,6 @@ public record HomeLatestVersionResponse( HomePlatformVersionResponse ios, - HomePlatformVersionResponse android, - - HomePlatformVersionResponse web + HomePlatformVersionResponse android ) { } diff --git a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql index 0557cc60..db278619 100644 --- a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql +++ b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql @@ -16,5 +16,4 @@ CREATE UNIQUE INDEX uk_app_versions_platform_latest INSERT INTO app_versions (platform, version, is_latest, summary, items) VALUES ('IOS', '1.2.0', true, '', '[]'::jsonb), - ('ANDROID', '1.2.0', true, '', '[]'::jsonb), - ('WEB', '1.2.0', true, '', '[]'::jsonb); + ('ANDROID', '1.2.0', true, '', '[]'::jsonb); From c46826dcaa5f020ddce2de223ccfc29287a03571 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 19:48:24 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20app=5Fversions=EC=97=90?= =?UTF-8?q?=EC=84=9C=20items=EB=A5=BC=20jsonb=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=ED=95=98=EC=97=AC=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app_versions 테이블에서 items(jsonb) 컬럼 제거, app_version_items 테이블 신설 등 --- .../application/AppVersionQueryService.java | 35 ++++++++++++---- .../appversion/core/entity/AppVersion.java | 8 ---- .../core/entity/AppVersionItem.java | 41 +++++++++++++++++-- .../repository/AppVersionItemRepository.java | 10 +++++ ...527_1600__IS_create_app_versions_table.sql | 23 +++++++++-- 5 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java index fadc9516..d9e9272e 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/application/AppVersionQueryService.java @@ -2,6 +2,8 @@ import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersionItem; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionItemRepository; import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; import com.devkor.ifive.nadab.domain.appversion.core.repository.UserAppVersionDismissalRepository; import com.devkor.ifive.nadab.domain.dailyreport.api.dto.response.HomeLatestVersionResponse; @@ -13,7 +15,6 @@ import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -23,6 +24,7 @@ public class AppVersionQueryService { private final AppVersionRepository appVersionRepository; + private final AppVersionItemRepository appVersionItemRepository; private final UserAppVersionDismissalRepository userAppVersionDismissalRepository; public HomeLatestVersionResponse getHomeLatestVersion(Long userId) { @@ -37,10 +39,27 @@ public HomeLatestVersionResponse getHomeLatestVersion(Long userId) { : userAppVersionDismissalRepository.findDismissedAppVersionIds(userId, appVersionIds).stream() .collect(Collectors.toSet()); + List versionItems = appVersionIds.isEmpty() + ? List.of() + : appVersionItemRepository.findByAppVersionIdInOrderByDisplayOrderAsc(appVersionIds); + + Map> itemResponsesByVersionId = versionItems.stream() + .collect(Collectors.groupingBy( + item -> item.getAppVersion().getId(), + Collectors.mapping( + item -> new HomeVersionItemResponse(item.getTitle(), item.getDescription()), + Collectors.toList() + ) + )); + Map latestVersionByPlatform = latestVersions.stream() .collect(Collectors.toMap( AppVersion::getPlatform, - appVersion -> toPlatformResponse(appVersion, dismissedAppVersionIds.contains(appVersion.getId())), + appVersion -> toPlatformResponse( + appVersion, + itemResponsesByVersionId.getOrDefault(appVersion.getId(), List.of()), + dismissedAppVersionIds.contains(appVersion.getId()) + ), (left, right) -> right )); @@ -50,13 +69,11 @@ public HomeLatestVersionResponse getHomeLatestVersion(Long userId) { ); } - private HomePlatformVersionResponse toPlatformResponse(AppVersion appVersion, boolean dismissed) { - List items = Optional.ofNullable(appVersion.getItems()) - .orElse(List.of()) - .stream() - .map(item -> new HomeVersionItemResponse(item.title(), item.description())) - .toList(); - + private HomePlatformVersionResponse toPlatformResponse( + AppVersion appVersion, + List items, + boolean dismissed + ) { return new HomePlatformVersionResponse( appVersion.getId(), appVersion.getVersion(), diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java index 3daec1e5..155e764d 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -1,14 +1,10 @@ package com.devkor.ifive.nadab.domain.appversion.core.entity; import com.devkor.ifive.nadab.global.shared.entity.AuditableEntity; -import io.hypersistence.utils.hibernate.type.json.JsonType; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.Type; - -import java.util.List; @Entity @Table( @@ -37,8 +33,4 @@ public class AppVersion extends AuditableEntity { @Column(name = "summary", nullable = false, length = 120) private String summary; - - @Type(JsonType.class) - @Column(name = "items", columnDefinition = "jsonb", nullable = false) - private List items; } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java index 0f4921ef..cdd39ef1 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java @@ -1,7 +1,40 @@ package com.devkor.ifive.nadab.domain.appversion.core.entity; -public record AppVersionItem( - String title, - String description -) { +import com.devkor.ifive.nadab.global.shared.entity.AuditableEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table( + name = "app_version_items", + uniqueConstraints = { + @UniqueConstraint(name = "uk_app_version_items_order", columnNames = {"app_version_id", "display_order"}) + } +) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AppVersionItem extends AuditableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn( + name = "app_version_id", + nullable = false, + foreignKey = @ForeignKey(name = "fk_app_version_items_app_version") + ) + private AppVersion appVersion; + + @Column(name = "title", nullable = false, length = 100) + private String title; + + @Column(name = "description", nullable = false, length = 500) + private String description; + + @Column(name = "display_order", nullable = false) + private Integer displayOrder; } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java new file mode 100644 index 00000000..f193f0f1 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java @@ -0,0 +1,10 @@ +package com.devkor.ifive.nadab.domain.appversion.core.repository; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersionItem; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface AppVersionItemRepository extends JpaRepository { + List findByAppVersionIdInOrderByDisplayOrderAsc(List appVersionIds); +} diff --git a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql index db278619..8e615730 100644 --- a/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql +++ b/src/main/resources/db/migration/V20260527_1600__IS_create_app_versions_table.sql @@ -4,16 +4,31 @@ CREATE TABLE app_versions ( version VARCHAR(30) NOT NULL, is_latest BOOLEAN NOT NULL, summary VARCHAR(120) NOT NULL, - items JSONB NOT NULL DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uk_app_versions_platform_version UNIQUE (platform, version) ); +CREATE TABLE app_version_items ( + id BIGSERIAL PRIMARY KEY, + app_version_id BIGINT NOT NULL, + title VARCHAR(100) NOT NULL, + description VARCHAR(500) NOT NULL, + display_order INT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT fk_app_version_items_app_version FOREIGN KEY (app_version_id) + REFERENCES app_versions(id) ON DELETE CASCADE, + CONSTRAINT uk_app_version_items_order UNIQUE (app_version_id, display_order) +); + +CREATE INDEX idx_app_version_items_app_version_id + ON app_version_items (app_version_id); + CREATE UNIQUE INDEX uk_app_versions_platform_latest ON app_versions (platform) WHERE is_latest = true; -INSERT INTO app_versions (platform, version, is_latest, summary, items) VALUES - ('IOS', '1.2.0', true, '', '[]'::jsonb), - ('ANDROID', '1.2.0', true, '', '[]'::jsonb); +INSERT INTO app_versions (platform, version, is_latest, summary) VALUES + ('IOS', '1.2.0', true, ''), + ('ANDROID', '1.2.0', true, ''); From 49992d37c7ee6cfd1f54545fb19409f5aeaf2340 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 20:30:32 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat(admin):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=EB=B3=B4?= =?UTF-8?q?=ED=98=B8=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commitlint.config.js | 4 + .../domain/admin/api/AdminAuthController.java | 52 +++++++++ .../api/dto/request/AdminLoginRequest.java | 9 ++ .../dto/response/AdminAuthStatusResponse.java | 6 ++ .../AdminPageAuthCommandService.java | 26 +++++ .../core/properties/AdminPageProperties.java | 26 +++++ .../security/AdminPageAuthCookieService.java | 43 ++++++++ .../security/AdminPageAuthInterceptor.java | 74 +++++++++++++ .../security/AdminPageAuthTokenService.java | 100 ++++++++++++++++++ .../infra/security/AdminPageWebMvcConfig.java | 19 ++++ .../nadab/global/core/response/ErrorCode.java | 6 +- src/main/resources/application.yml | 8 +- 12 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminLoginRequest.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAuthStatusResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminPageAuthCommandService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/core/properties/AdminPageProperties.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthCookieService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthInterceptor.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthTokenService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageWebMvcConfig.java diff --git a/commitlint.config.js b/commitlint.config.js index bb7e447a..47cc82aa 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -14,6 +14,7 @@ module.exports = { "revert" ]], "scope-enum": [2, "always", [ + "admin", "auth", "user", "ai", @@ -100,6 +101,9 @@ module.exports = { scope: { description: '[Scope] 이번 변경이 적용된 범위를 선택해주세요 (범위 생략하려면 empty 선택)', enum: { + admin: { + description: '🧑‍💻 어드민 도메인 (예: 어드민 페이지)' + }, auth: { description: '🔐 인증/인가 도메인 (예: OAuth2, JWT, 세션)' }, diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java new file mode 100644 index 00000000..cb102f2b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java @@ -0,0 +1,52 @@ +package com.devkor.ifive.nadab.domain.admin.api; + +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminLoginRequest; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAuthStatusResponse; +import com.devkor.ifive.nadab.domain.admin.application.AdminPageAuthCommandService; +import com.devkor.ifive.nadab.domain.admin.infra.security.AdminPageAuthCookieService; +import com.devkor.ifive.nadab.domain.admin.infra.security.AdminPageAuthTokenService; +import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; +import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; +import jakarta.annotation.security.PermitAll; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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 +@RequestMapping("/admin/api") +@RequiredArgsConstructor +public class AdminAuthController { + + private final AdminPageAuthCommandService adminPageAuthCommandService; + private final AdminPageAuthTokenService adminPageAuthTokenService; + private final AdminPageAuthCookieService adminPageAuthCookieService; + + @PostMapping("/login") + @PermitAll + public ResponseEntity> login( + @RequestBody @Valid AdminLoginRequest request, + HttpServletResponse response + ) { + adminPageAuthCommandService.validatePassword(request.password()); + String token = adminPageAuthTokenService.issueToken(); + adminPageAuthCookieService.addCookie(response, token); + return ApiResponseEntity.noContent(); + } + + @PostMapping("/logout") + public ResponseEntity> logout(HttpServletResponse response) { + adminPageAuthCookieService.expireCookie(response); + return ApiResponseEntity.noContent(); + } + + @GetMapping("/auth-status") + public ResponseEntity> authStatus() { + return ApiResponseEntity.ok(new AdminAuthStatusResponse(true)); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminLoginRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminLoginRequest.java new file mode 100644 index 00000000..1fd2b85b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminLoginRequest.java @@ -0,0 +1,9 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record AdminLoginRequest( + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAuthStatusResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAuthStatusResponse.java new file mode 100644 index 00000000..ab53f152 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminAuthStatusResponse.java @@ -0,0 +1,6 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +public record AdminAuthStatusResponse( + boolean authenticated +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminPageAuthCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminPageAuthCommandService.java new file mode 100644 index 00000000..4e236b3e --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminPageAuthCommandService.java @@ -0,0 +1,26 @@ +package com.devkor.ifive.nadab.domain.admin.application; + +import com.devkor.ifive.nadab.domain.admin.core.properties.AdminPageProperties; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +@Service +@RequiredArgsConstructor +public class AdminPageAuthCommandService { + + private final AdminPageProperties adminPageProperties; + + public void validatePassword(String rawPassword) { + byte[] input = rawPassword.getBytes(StandardCharsets.UTF_8); + byte[] expected = adminPageProperties.getPassword().getBytes(StandardCharsets.UTF_8); + + if (!MessageDigest.isEqual(input, expected)) { + throw new UnauthorizedException(ErrorCode.ADMIN_PAGE_INVALID_PASSWORD); + } + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/core/properties/AdminPageProperties.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/core/properties/AdminPageProperties.java new file mode 100644 index 00000000..4db600df --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/core/properties/AdminPageProperties.java @@ -0,0 +1,26 @@ +package com.devkor.ifive.nadab.domain.admin.core.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "admin.page") +public class AdminPageProperties { + + @NotBlank + private String password; + + @Min(60) + private long tokenExpirationSeconds = 43200; + + @NotBlank + private String cookieName = "admin_auth"; +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthCookieService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthCookieService.java new file mode 100644 index 00000000..3e73840f --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthCookieService.java @@ -0,0 +1,43 @@ +package com.devkor.ifive.nadab.domain.admin.infra.security; + +import com.devkor.ifive.nadab.domain.admin.core.properties.AdminPageProperties; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AdminPageAuthCookieService { + + @Value("${app.cookie.secure}") + private boolean cookieSecure; + + @Value("${app.cookie.same-site}") + private String cookieSameSite; + + private final AdminPageProperties adminPageProperties; + + public void addCookie(HttpServletResponse response, String token) { + ResponseCookie cookie = ResponseCookie.from(adminPageProperties.getCookieName(), token) + .httpOnly(true) + .secure(cookieSecure) + .sameSite(cookieSameSite) + .path("/admin") + .maxAge(adminPageProperties.getTokenExpirationSeconds()) + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } + + public void expireCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(adminPageProperties.getCookieName(), "") + .httpOnly(true) + .secure(cookieSecure) + .sameSite(cookieSameSite) + .path("/admin") + .maxAge(0) + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthInterceptor.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthInterceptor.java new file mode 100644 index 00000000..76578edd --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthInterceptor.java @@ -0,0 +1,74 @@ +package com.devkor.ifive.nadab.domain.admin.infra.security; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class AdminPageAuthInterceptor implements HandlerInterceptor { + + private final AdminPageAuthTokenService adminPageAuthTokenService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + return true; + } + + String requestUri = request.getRequestURI(); + String token = extractCookieValue(request, adminPageAuthTokenService.getCookieName()); + boolean isTokenValid = adminPageAuthTokenService.isValid(token); + + if (isLoginApi(requestUri)) { + return true; + } + + if (isLoginPage(requestUri)) { + if (isTokenValid) { + response.sendRedirect("/admin"); + return false; + } + return true; + } + + if (isTokenValid) { + return true; + } + + if (requestUri.startsWith("/admin/api/")) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + + response.sendRedirect("/admin/login"); + return false; + } + + private boolean isLoginApi(String requestUri) { + return "/admin/api/login".equals(requestUri); + } + + private boolean isLoginPage(String requestUri) { + return "/admin/login".equals(requestUri) || requestUri.startsWith("/admin/login/"); + } + + private String extractCookieValue(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthTokenService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthTokenService.java new file mode 100644 index 00000000..9a31db42 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageAuthTokenService.java @@ -0,0 +1,100 @@ +package com.devkor.ifive.nadab.domain.admin.infra.security; + +import com.devkor.ifive.nadab.domain.admin.core.properties.AdminPageProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.util.Base64; + +@Component +@RequiredArgsConstructor +public class AdminPageAuthTokenService { + + private static final String HMAC_ALGORITHM = "HmacSHA256"; + + private final AdminPageProperties adminPageProperties; + + public String issueToken() { + long issuedAt = Instant.now().getEpochSecond(); + long expiresAt = issuedAt + adminPageProperties.getTokenExpirationSeconds(); + + String payload = issuedAt + ":" + expiresAt; + String encodedPayload = encode(payload.getBytes(StandardCharsets.UTF_8)); + String signature = sign(encodedPayload); + + return encodedPayload + "." + signature; + } + + public boolean isValid(String token) { + if (token == null || token.isBlank()) { + return false; + } + + String[] parts = token.split("\\."); + if (parts.length != 2) { + return false; + } + + String encodedPayload = parts[0]; + String signature = parts[1]; + String expectedSignature = sign(encodedPayload); + + if (!MessageDigest.isEqual( + signature.getBytes(StandardCharsets.UTF_8), + expectedSignature.getBytes(StandardCharsets.UTF_8) + )) { + return false; + } + + String decodedPayload; + try { + decodedPayload = new String( + Base64.getUrlDecoder().decode(encodedPayload), + StandardCharsets.UTF_8 + ); + } catch (IllegalArgumentException e) { + return false; + } + + String[] payloadParts = decodedPayload.split(":"); + if (payloadParts.length != 2) { + return false; + } + + long expiresAt; + try { + expiresAt = Long.parseLong(payloadParts[1]); + } catch (NumberFormatException e) { + return false; + } + + return Instant.now().getEpochSecond() < expiresAt; + } + + public String getCookieName() { + return adminPageProperties.getCookieName(); + } + + private String sign(String encodedPayload) { + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + mac.init(new SecretKeySpec( + adminPageProperties.getPassword().getBytes(StandardCharsets.UTF_8), + HMAC_ALGORITHM + )); + byte[] signature = mac.doFinal(encodedPayload.getBytes(StandardCharsets.UTF_8)); + return encode(signature); + } catch (Exception e) { + throw new IllegalStateException("Admin token signing failed", e); + } + } + + private String encode(byte[] bytes) { + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageWebMvcConfig.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageWebMvcConfig.java new file mode 100644 index 00000000..c10d71b2 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/infra/security/AdminPageWebMvcConfig.java @@ -0,0 +1,19 @@ +package com.devkor.ifive.nadab.domain.admin.infra.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class AdminPageWebMvcConfig implements WebMvcConfigurer { + + private final AdminPageAuthInterceptor adminPageAuthInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminPageAuthInterceptor) + .addPathPatterns("/admin/**"); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java index b6e2d5a4..23a105df 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java +++ b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java @@ -420,7 +420,11 @@ public enum ErrorCode { // ==================== APP_VERSION (앱 버전) ==================== // 404 Not Found - APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전을 찾을 수 없습니다"); + APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전을 찾을 수 없습니다"), + + // ==================== ADMIN (어드민) ==================== + // 401 Unauthorized + ADMIN_PAGE_INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "관리자 페이지 비밀번호가 올바르지 않습니다"); private final HttpStatus httpStatus; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f05305aa..a3a30f59 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -83,6 +83,12 @@ app: secure: false same-site: Lax +admin: + page: + password: ${ADMIN_PAGE_PASSWORD} + token-expiration-seconds: 43200 + cookie-name: admin_auth + api_prefix: /api/v1 firebase: @@ -95,4 +101,4 @@ openai: model: gpt-image-1.5 size: 1024x1024 quality: low - output-format: webp \ No newline at end of file + output-format: webp From e4f476f7313cab6c431a094113883368d4d2a0bc Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 20:40:15 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat(admin):=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=A1=B0=ED=9A=8C/=EC=83=9D=EC=84=B1=20API=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 --- .../admin/api/AdminVersionController.java | 39 +++++++++++ .../request/AdminVersionCreateRequest.java | 20 ++++++ .../response/AdminLatestVersionsResponse.java | 8 +++ .../response/AdminVersionCreateResponse.java | 6 ++ .../response/AdminVersionItemResponse.java | 9 +++ .../dto/response/AdminVersionResponse.java | 14 ++++ .../AdminVersionCommandService.java | 40 ++++++++++++ .../application/AdminVersionQueryService.java | 64 +++++++++++++++++++ .../appversion/core/entity/AppVersion.java | 19 ++++++ .../core/repository/AppVersionRepository.java | 6 ++ .../nadab/global/core/response/ErrorCode.java | 3 + 11 files changed, 228 insertions(+) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionCreateRequest.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminLatestVersionsResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionCreateResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionQueryService.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java new file mode 100644 index 00000000..7dcfc36b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java @@ -0,0 +1,39 @@ +package com.devkor.ifive.nadab.domain.admin.api; + +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminLatestVersionsResponse; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionCreateResponse; +import com.devkor.ifive.nadab.domain.admin.application.AdminVersionCommandService; +import com.devkor.ifive.nadab.domain.admin.application.AdminVersionQueryService; +import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; +import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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 +@RequestMapping("/admin/api/versions") +@RequiredArgsConstructor +public class AdminVersionController { + + private final AdminVersionQueryService adminVersionQueryService; + private final AdminVersionCommandService adminVersionCommandService; + + @GetMapping("/latest") + public ResponseEntity> getLatestVersions() { + return ApiResponseEntity.ok(adminVersionQueryService.getLatestVersions()); + } + + @PostMapping + public ResponseEntity> createVersion( + @RequestBody @Valid AdminVersionCreateRequest request + ) { + Long appVersionId = adminVersionCommandService.createVersion(request); + return ApiResponseEntity.created(new AdminVersionCreateResponse(appVersionId)); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionCreateRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionCreateRequest.java new file mode 100644 index 00000000..dad51c94 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionCreateRequest.java @@ -0,0 +1,20 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.request; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record AdminVersionCreateRequest( + @NotNull(message = "플랫폼은 필수입니다.") + AppPlatform platform, + + @NotBlank(message = "버전은 필수입니다.") + @Size(max = 30, message = "버전은 30자 이하여야 합니다.") + String version, + + @NotNull(message = "요약은 null일 수 없습니다.") + @Size(max = 120, message = "요약은 120자 이하여야 합니다.") + String summary +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminLatestVersionsResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminLatestVersionsResponse.java new file mode 100644 index 00000000..cca38a31 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminLatestVersionsResponse.java @@ -0,0 +1,8 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +import java.util.List; + +public record AdminLatestVersionsResponse( + List versions +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionCreateResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionCreateResponse.java new file mode 100644 index 00000000..aed9aa4a --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionCreateResponse.java @@ -0,0 +1,6 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +public record AdminVersionCreateResponse( + Long appVersionId +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemResponse.java new file mode 100644 index 00000000..b90e938f --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemResponse.java @@ -0,0 +1,9 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +public record AdminVersionItemResponse( + Long id, + String title, + String description, + Integer displayOrder +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionResponse.java new file mode 100644 index 00000000..01bc69c9 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionResponse.java @@ -0,0 +1,14 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; + +import java.util.List; + +public record AdminVersionResponse( + Long id, + AppPlatform platform, + String version, + String summary, + List items +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java new file mode 100644 index 00000000..0d06b5df --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java @@ -0,0 +1,40 @@ +package com.devkor.ifive.nadab.domain.admin.application; + +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.ConflictException; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class AdminVersionCommandService { + + private final AppVersionRepository appVersionRepository; + + public Long createVersion(AdminVersionCreateRequest request) { + if (appVersionRepository.existsByPlatformAndVersion(request.platform(), request.version())) { + throw new ConflictException(ErrorCode.APP_VERSION_ALREADY_EXISTS); + } + + appVersionRepository.findByPlatformAndIsLatestTrue(request.platform()) + .ifPresent(AppVersion::markAsNotLatest); + + AppVersion appVersion = AppVersion.create( + request.platform(), + request.version(), + request.summary() + ); + try { + appVersionRepository.save(appVersion); + return appVersion.getId(); + } catch (DataIntegrityViolationException e) { + throw new ConflictException(ErrorCode.APP_VERSION_ALREADY_EXISTS); + } + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionQueryService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionQueryService.java new file mode 100644 index 00000000..6b470c35 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionQueryService.java @@ -0,0 +1,64 @@ +package com.devkor.ifive.nadab.domain.admin.application; + +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminLatestVersionsResponse; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionItemResponse; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionResponse; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersionItem; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionItemRepository; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminVersionQueryService { + + private final AppVersionRepository appVersionRepository; + private final AppVersionItemRepository appVersionItemRepository; + + public AdminLatestVersionsResponse getLatestVersions() { + List latestVersions = appVersionRepository.findByIsLatestTrue(); + List appVersionIds = latestVersions.stream() + .map(AppVersion::getId) + .toList(); + + List items = appVersionIds.isEmpty() + ? List.of() + : appVersionItemRepository.findByAppVersionIdInOrderByDisplayOrderAsc(appVersionIds); + + Map> itemsByVersionId = items.stream() + .collect(Collectors.groupingBy( + item -> item.getAppVersion().getId(), + Collectors.mapping( + item -> new AdminVersionItemResponse( + item.getId(), + item.getTitle(), + item.getDescription(), + item.getDisplayOrder() + ), + Collectors.toList() + ) + )); + + List versions = latestVersions.stream() + .sorted(Comparator.comparing(version -> version.getPlatform().name())) + .map(version -> new AdminVersionResponse( + version.getId(), + version.getPlatform(), + version.getVersion(), + version.getSummary(), + itemsByVersionId.getOrDefault(version.getId(), List.of()) + )) + .toList(); + + return new AdminLatestVersionsResponse(versions); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java index 155e764d..56c86608 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -33,4 +33,23 @@ public class AppVersion extends AuditableEntity { @Column(name = "summary", nullable = false, length = 120) private String summary; + + private AppVersion(AppPlatform platform, String version, boolean isLatest, String summary) { + this.platform = platform; + this.version = version; + this.isLatest = isLatest; + this.summary = summary; + } + + public static AppVersion create(AppPlatform platform, String version, String summary) { + return new AppVersion(platform, version, true, summary); + } + + public void markAsLatest() { + this.isLatest = true; + } + + public void markAsNotLatest() { + this.isLatest = false; + } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java index c9f7ba80..dd437ffd 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionRepository.java @@ -1,10 +1,16 @@ package com.devkor.ifive.nadab.domain.appversion.core.repository; import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface AppVersionRepository extends JpaRepository { List findByIsLatestTrue(); + + Optional findByPlatformAndIsLatestTrue(AppPlatform platform); + + boolean existsByPlatformAndVersion(AppPlatform platform, String version); } diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java index 23a105df..3276b300 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java +++ b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java @@ -422,6 +422,9 @@ public enum ErrorCode { // 404 Not Found APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전을 찾을 수 없습니다"), + // 409 Conflict + APP_VERSION_ALREADY_EXISTS(HttpStatus.CONFLICT, "해당 플랫폼의 같은 버전이 이미 존재합니다"), + // ==================== ADMIN (어드민) ==================== // 401 Unauthorized ADMIN_PAGE_INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "관리자 페이지 비밀번호가 올바르지 않습니다"); From faf3fe9c6e4cf926070ed412cbaed373481ba7b4 Mon Sep 17 00:00:00 2001 From: 1Seob Date: Sun, 31 May 2026 20:44:58 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat(admin):=20=EC=95=B1=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=ED=95=AD=EB=AA=A9(app=5Fversion=5Fitems)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80/=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/api/AdminVersionController.java | 33 ++++++++++ .../AdminVersionItemUpsertRequest.java | 23 +++++++ .../AdminVersionItemCreateResponse.java | 6 ++ .../AdminVersionItemCommandService.java | 66 +++++++++++++++++++ .../core/entity/AppVersionItem.java | 17 +++++ .../repository/AppVersionItemRepository.java | 4 ++ .../nadab/global/core/response/ErrorCode.java | 2 + 7 files changed, 151 insertions(+) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionItemUpsertRequest.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemCreateResponse.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java index 7dcfc36b..6d4d05df 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java @@ -1,18 +1,24 @@ package com.devkor.ifive.nadab.domain.admin.api; import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest; +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionItemUpsertRequest; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminLatestVersionsResponse; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionCreateResponse; +import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionItemCreateResponse; import com.devkor.ifive.nadab.domain.admin.application.AdminVersionCommandService; +import com.devkor.ifive.nadab.domain.admin.application.AdminVersionItemCommandService; import com.devkor.ifive.nadab.domain.admin.application.AdminVersionQueryService; import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,6 +29,7 @@ public class AdminVersionController { private final AdminVersionQueryService adminVersionQueryService; private final AdminVersionCommandService adminVersionCommandService; + private final AdminVersionItemCommandService adminVersionItemCommandService; @GetMapping("/latest") public ResponseEntity> getLatestVersions() { @@ -36,4 +43,30 @@ public ResponseEntity> createVersion( Long appVersionId = adminVersionCommandService.createVersion(request); return ApiResponseEntity.created(new AdminVersionCreateResponse(appVersionId)); } + + @PostMapping("/{appVersionId}/items") + public ResponseEntity> createVersionItem( + @PathVariable Long appVersionId, + @RequestBody @Valid AdminVersionItemUpsertRequest request + ) { + Long appVersionItemId = adminVersionItemCommandService.createItem(appVersionId, request); + return ApiResponseEntity.created(new AdminVersionItemCreateResponse(appVersionItemId)); + } + + @PutMapping("/items/{appVersionItemId}") + public ResponseEntity> updateVersionItem( + @PathVariable Long appVersionItemId, + @RequestBody @Valid AdminVersionItemUpsertRequest request + ) { + adminVersionItemCommandService.updateItem(appVersionItemId, request); + return ApiResponseEntity.noContent(); + } + + @DeleteMapping("/items/{appVersionItemId}") + public ResponseEntity> deleteVersionItem( + @PathVariable Long appVersionItemId + ) { + adminVersionItemCommandService.deleteItem(appVersionItemId); + return ApiResponseEntity.noContent(); + } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionItemUpsertRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionItemUpsertRequest.java new file mode 100644 index 00000000..6297f4d7 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionItemUpsertRequest.java @@ -0,0 +1,23 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record AdminVersionItemUpsertRequest( + @NotBlank(message = "업데이트명은 필수입니다.") + @Size(max = 100, message = "업데이트명은 100자 이하여야 합니다.") + String title, + + @NotBlank(message = "상세 내용은 필수입니다.") + @Size(max = 500, message = "상세 내용은 500자 이하여야 합니다.") + String description, + + @NotNull(message = "displayOrder는 필수입니다.") + @Min(value = 1, message = "displayOrder는 1 이상이어야 합니다.") + @Max(value = 9999, message = "displayOrder는 9999 이하여야 합니다.") + Integer displayOrder +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemCreateResponse.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemCreateResponse.java new file mode 100644 index 00000000..394facd5 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/response/AdminVersionItemCreateResponse.java @@ -0,0 +1,6 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.response; + +public record AdminVersionItemCreateResponse( + Long appVersionItemId +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java new file mode 100644 index 00000000..8bb0b6e8 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java @@ -0,0 +1,66 @@ +package com.devkor.ifive.nadab.domain.admin.application; + +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionItemUpsertRequest; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion; +import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersionItem; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionItemRepository; +import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; +import com.devkor.ifive.nadab.global.core.response.ErrorCode; +import com.devkor.ifive.nadab.global.exception.ConflictException; +import com.devkor.ifive.nadab.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class AdminVersionItemCommandService { + + private final AppVersionRepository appVersionRepository; + private final AppVersionItemRepository appVersionItemRepository; + + public Long createItem(Long appVersionId, AdminVersionItemUpsertRequest request) { + AppVersion appVersion = appVersionRepository.findById(appVersionId) + .orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_NOT_FOUND)); + + if (appVersionItemRepository.existsByAppVersionIdAndDisplayOrder(appVersionId, request.displayOrder())) { + throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); + } + + AppVersionItem item = AppVersionItem.create( + appVersion, + request.title(), + request.description(), + request.displayOrder() + ); + + try { + appVersionItemRepository.save(item); + return item.getId(); + } catch (DataIntegrityViolationException e) { + throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); + } + } + + public void updateItem(Long appVersionItemId, AdminVersionItemUpsertRequest request) { + AppVersionItem item = appVersionItemRepository.findById(appVersionItemId) + .orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_ITEM_NOT_FOUND)); + + Long appVersionId = item.getAppVersion().getId(); + if (appVersionItemRepository.existsByAppVersionIdAndDisplayOrderAndIdNot( + appVersionId, request.displayOrder(), appVersionItemId + )) { + throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); + } + + item.update(request.title(), request.description(), request.displayOrder()); + } + + public void deleteItem(Long appVersionItemId) { + AppVersionItem item = appVersionItemRepository.findById(appVersionItemId) + .orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_ITEM_NOT_FOUND)); + appVersionItemRepository.delete(item); + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java index cdd39ef1..f63f6fe5 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersionItem.java @@ -37,4 +37,21 @@ public class AppVersionItem extends AuditableEntity { @Column(name = "display_order", nullable = false) private Integer displayOrder; + + private AppVersionItem(AppVersion appVersion, String title, String description, Integer displayOrder) { + this.appVersion = appVersion; + this.title = title; + this.description = description; + this.displayOrder = displayOrder; + } + + public static AppVersionItem create(AppVersion appVersion, String title, String description, Integer displayOrder) { + return new AppVersionItem(appVersion, title, description, displayOrder); + } + + public void update(String title, String description, Integer displayOrder) { + this.title = title; + this.description = description; + this.displayOrder = displayOrder; + } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java index f193f0f1..664fc317 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/repository/AppVersionItemRepository.java @@ -7,4 +7,8 @@ public interface AppVersionItemRepository extends JpaRepository { List findByAppVersionIdInOrderByDisplayOrderAsc(List appVersionIds); + + boolean existsByAppVersionIdAndDisplayOrder(Long appVersionId, Integer displayOrder); + + boolean existsByAppVersionIdAndDisplayOrderAndIdNot(Long appVersionId, Integer displayOrder, Long id); } diff --git a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java index 3276b300..bbaed66d 100644 --- a/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java +++ b/src/main/java/com/devkor/ifive/nadab/global/core/response/ErrorCode.java @@ -421,9 +421,11 @@ public enum ErrorCode { // ==================== APP_VERSION (앱 버전) ==================== // 404 Not Found APP_VERSION_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전을 찾을 수 없습니다"), + APP_VERSION_ITEM_NOT_FOUND(HttpStatus.NOT_FOUND, "앱 버전 항목을 찾을 수 없습니다"), // 409 Conflict APP_VERSION_ALREADY_EXISTS(HttpStatus.CONFLICT, "해당 플랫폼의 같은 버전이 이미 존재합니다"), + APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED(HttpStatus.CONFLICT, "같은 버전 내 displayOrder가 중복됩니다"), // ==================== ADMIN (어드민) ==================== // 401 Unauthorized From a8415f382c93adf5f7d12acbdb71691d11844c1b Mon Sep 17 00:00:00 2001 From: 1Seob Date: Mon, 1 Jun 2026 11:05:20 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat(admin):=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=EB=A6=AC=ED=94=84=20=EA=B8=B0=EB=B0=98=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EB=B2=84=EC=A0=84=20=EA=B4=80=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=B6=95=20=EB=B0=8F=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20UX=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타임리프 어드민 페이지 구현 : 관리자 로그인 페이지 추가, 버전 관리 페이지 추가, 백엔드 연동 보강 --- .github/workflows/deploy-to-dev-ec2.yml | 3 +- .github/workflows/deploy-to-prod-ec2.yml | 1 + .../domain/admin/api/AdminAuthController.java | 2 + .../domain/admin/api/AdminPageController.java | 23 + .../admin/api/AdminVersionController.java | 12 + .../AdminVersionSummaryUpdateRequest.java | 11 + .../AdminVersionCommandService.java | 10 +- .../AdminVersionItemCommandService.java | 9 +- .../appversion/core/entity/AppVersion.java | 4 + src/main/resources/templates/admin/login.html | 164 ++++ .../resources/templates/admin/version.html | 735 ++++++++++++++++++ 11 files changed, 970 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminPageController.java create mode 100644 src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionSummaryUpdateRequest.java create mode 100644 src/main/resources/templates/admin/login.html create mode 100644 src/main/resources/templates/admin/version.html diff --git a/.github/workflows/deploy-to-dev-ec2.yml b/.github/workflows/deploy-to-dev-ec2.yml index 84b57c76..f7975e20 100644 --- a/.github/workflows/deploy-to-dev-ec2.yml +++ b/.github/workflows/deploy-to-dev-ec2.yml @@ -147,7 +147,8 @@ jobs: ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" \ GOOGLE_GENAI_API_KEY="${{ secrets.GOOGLE_GENAI_API_KEY }}" \ FIREBASE_ADMIN_KEY="${{ secrets.FIREBASE_ADMIN_KEY }}" \ + ADMIN_PAGE_PASSWORD="${{ secrets.ADMIN_PAGE_PASSWORD }}" \ nohup java -jar "$JAR_PATH" \ --spring.profiles.active=dev > app.log 2>&1 & - echo "✅ 배포 완료!" \ No newline at end of file + echo "✅ 배포 완료!" diff --git a/.github/workflows/deploy-to-prod-ec2.yml b/.github/workflows/deploy-to-prod-ec2.yml index 8f0220fd..bf2f3125 100644 --- a/.github/workflows/deploy-to-prod-ec2.yml +++ b/.github/workflows/deploy-to-prod-ec2.yml @@ -144,6 +144,7 @@ jobs: ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" \ GOOGLE_GENAI_API_KEY="${{ secrets.GOOGLE_GENAI_API_KEY }}" \ FIREBASE_ADMIN_KEY="${{ secrets.FIREBASE_ADMIN_KEY }}" \ + ADMIN_PAGE_PASSWORD="${{ secrets.ADMIN_PAGE_PASSWORD }}" \ nohup java -jar "$JAR_PATH" \ --spring.profiles.active=prod > app-prod.log 2>&1 & diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java index cb102f2b..2787dd98 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminAuthController.java @@ -7,6 +7,7 @@ import com.devkor.ifive.nadab.domain.admin.infra.security.AdminPageAuthTokenService; import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; +import io.swagger.v3.oas.annotations.Hidden; import jakarta.annotation.security.PermitAll; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Hidden @RestController @RequestMapping("/admin/api") @RequiredArgsConstructor diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminPageController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminPageController.java new file mode 100644 index 00000000..54b8ad5b --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminPageController.java @@ -0,0 +1,23 @@ +package com.devkor.ifive.nadab.domain.admin.api; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class AdminPageController { + + @GetMapping("/admin/login") + public String loginPage() { + return "admin/login"; + } + + @GetMapping("/admin") + public String adminRoot() { + return "redirect:/admin/tabs/app-versions"; + } + + @GetMapping("/admin/tabs/app-versions") + public String adminVersionPage() { + return "admin/version"; + } +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java index 6d4d05df..7859ce12 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/AdminVersionController.java @@ -2,6 +2,7 @@ import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest; import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionItemUpsertRequest; +import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionSummaryUpdateRequest; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminLatestVersionsResponse; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionCreateResponse; import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionItemCreateResponse; @@ -10,6 +11,7 @@ import com.devkor.ifive.nadab.domain.admin.application.AdminVersionQueryService; import com.devkor.ifive.nadab.global.core.response.ApiResponseDto; import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity; +import io.swagger.v3.oas.annotations.Hidden; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Hidden @RestController @RequestMapping("/admin/api/versions") @RequiredArgsConstructor @@ -44,6 +47,15 @@ public ResponseEntity> createVersion( return ApiResponseEntity.created(new AdminVersionCreateResponse(appVersionId)); } + @PutMapping("/{appVersionId}/summary") + public ResponseEntity> updateVersionSummary( + @PathVariable Long appVersionId, + @RequestBody @Valid AdminVersionSummaryUpdateRequest request + ) { + adminVersionCommandService.updateSummary(appVersionId, request.summary()); + return ApiResponseEntity.noContent(); + } + @PostMapping("/{appVersionId}/items") public ResponseEntity> createVersionItem( @PathVariable Long appVersionId, diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionSummaryUpdateRequest.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionSummaryUpdateRequest.java new file mode 100644 index 00000000..bad6e1c2 --- /dev/null +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/api/dto/request/AdminVersionSummaryUpdateRequest.java @@ -0,0 +1,11 @@ +package com.devkor.ifive.nadab.domain.admin.api.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record AdminVersionSummaryUpdateRequest( + @NotNull(message = "요약은 null일 수 없습니다.") + @Size(max = 120, message = "요약은 120자 이하여야 합니다.") + String summary +) { +} diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java index 0d06b5df..6f760a7e 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionCommandService.java @@ -5,6 +5,7 @@ import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository; import com.devkor.ifive.nadab.global.core.response.ErrorCode; import com.devkor.ifive.nadab.global.exception.ConflictException; +import com.devkor.ifive.nadab.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; @@ -24,6 +25,7 @@ public Long createVersion(AdminVersionCreateRequest request) { appVersionRepository.findByPlatformAndIsLatestTrue(request.platform()) .ifPresent(AppVersion::markAsNotLatest); + appVersionRepository.flush(); AppVersion appVersion = AppVersion.create( request.platform(), @@ -31,10 +33,16 @@ public Long createVersion(AdminVersionCreateRequest request) { request.summary() ); try { - appVersionRepository.save(appVersion); + appVersionRepository.saveAndFlush(appVersion); return appVersion.getId(); } catch (DataIntegrityViolationException e) { throw new ConflictException(ErrorCode.APP_VERSION_ALREADY_EXISTS); } } + + public void updateSummary(Long appVersionId, String summary) { + AppVersion appVersion = appVersionRepository.findById(appVersionId) + .orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_NOT_FOUND)); + appVersion.updateSummary(summary); + } } diff --git a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java index 8bb0b6e8..e5a67005 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/admin/application/AdminVersionItemCommandService.java @@ -37,7 +37,7 @@ public Long createItem(Long appVersionId, AdminVersionItemUpsertRequest request) ); try { - appVersionItemRepository.save(item); + appVersionItemRepository.saveAndFlush(item); return item.getId(); } catch (DataIntegrityViolationException e) { throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); @@ -55,7 +55,12 @@ public void updateItem(Long appVersionItemId, AdminVersionItemUpsertRequest requ throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); } - item.update(request.title(), request.description(), request.displayOrder()); + try { + item.update(request.title(), request.description(), request.displayOrder()); + appVersionItemRepository.flush(); + } catch (DataIntegrityViolationException e) { + throw new ConflictException(ErrorCode.APP_VERSION_ITEM_DISPLAY_ORDER_DUPLICATED); + } } public void deleteItem(Long appVersionItemId) { diff --git a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java index 56c86608..49a3d47b 100644 --- a/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java +++ b/src/main/java/com/devkor/ifive/nadab/domain/appversion/core/entity/AppVersion.java @@ -52,4 +52,8 @@ public void markAsLatest() { public void markAsNotLatest() { this.isLatest = false; } + + public void updateSummary(String summary) { + this.summary = summary; + } } diff --git a/src/main/resources/templates/admin/login.html b/src/main/resources/templates/admin/login.html new file mode 100644 index 00000000..0858d5c0 --- /dev/null +++ b/src/main/resources/templates/admin/login.html @@ -0,0 +1,164 @@ + + + + + + NADAB Admin Login + + + +
+

관리자 로그인

+

비밀번호를 입력해 버전 관리 페이지에 접근합니다.

+
+ + + +
+
+
+ + + + diff --git a/src/main/resources/templates/admin/version.html b/src/main/resources/templates/admin/version.html new file mode 100644 index 00000000..db82efe4 --- /dev/null +++ b/src/main/resources/templates/admin/version.html @@ -0,0 +1,735 @@ + + + + + + NADAB Admin Version + + + +
+
+

Admin Console

+ +
+ + + +
+

Create Version

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

Current Latest Versions

+
+
+
+ + + + + +