From ea85b99075852e5c30c7ba9cf01f8621f9b67d6a Mon Sep 17 00:00:00 2001 From: si-zero Date: Sat, 24 Oct 2026 08:17:24 +0900 Subject: [PATCH] =?UTF-8?q?#571=20[Feat]=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EB=B0=B0=ED=8B=80=20=EB=AA=A9=EB=A1=9D=20=EC=88=98=EC=A4=80=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C=20=EB=B0=8F=20=EA=B8=B0=EC=A1=B4=20=EB=B0=B0?= =?UTF-8?q?=ED=8B=80=20=EC=88=98=EC=A4=80=20=EC=B4=88=EA=B8=89=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-specs/battle-api.md | 2 ++ docs/api-specs/class-api.md | 1 + docs/erd/class.puml | 1 + .../domain/battle/converter/BattleConverter.java | 3 ++- .../battle/dto/response/BattleSimpleResponse.java | 4 +++- .../domain/battle/service/BattleServiceImpl.java | 3 +++ .../V20261024_01__battles_difficulty_beginner.sql | 3 +++ ...0261024_rollback__battles_difficulty_beginner.sql | 3 +++ .../AdminContentCreationIntegrationTest.java | 2 ++ .../ClassBattleRecommendationApiIntegrationTest.java | 12 ++++++++++++ 10 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/db/migration/V20261024_01__battles_difficulty_beginner.sql create mode 100644 src/main/resources/db/migration/V20261024_rollback__battles_difficulty_beginner.sql diff --git a/docs/api-specs/battle-api.md b/docs/api-specs/battle-api.md index 6d918e9a..5299571b 100644 --- a/docs/api-specs/battle-api.md +++ b/docs/api-specs/battle-api.md @@ -86,6 +86,8 @@ - `page` (기본값: `1`) - `size` (기본값: `10`) - `status` (선택) +- 항목(`BattleSimpleResponse`): `battleId`, `title`, `thumbnailUrl`, `status`, `publishAt`, `createdAt`, `difficulty` + - `difficulty`: 대상 수준 (`BEGINNER` 초급 / `INTERMEDIATE` 중급 / `ADVANCED` 고급). 새로 만든 배틀은 초급으로 저장되고, [클래스 설정 수정](./class-api.md#91-배틀-클래스-설정-수정)으로 바꾼다. ### 2.4 배틀 상세 - `GET /api/v1/admin/battles/{battleId}` diff --git a/docs/api-specs/class-api.md b/docs/api-specs/class-api.md index 1c40c623..b5168630 100644 --- a/docs/api-specs/class-api.md +++ b/docs/api-specs/class-api.md @@ -1074,6 +1074,7 @@ AI가 주제 후보를 제안하면 운영자가 하나를 고르거나 직접 - 응답: `AdminBattleDetailResponse`. 끝에 `difficulty`, `category`, `classRecommendable`, `midVoteEnabled`가 추가됐다. 어드민 배틀 상세 조회에도 같은 필드가 내려간다. - 추천 API는 이 설정이 켜진 배틀만 돌려준다. 그래서 운영에서 배틀마다 설정해야 추천 결과가 나온다. +- 수준은 기획 확정 전까지 임시로 초급이다. 수준이 비어 있던 기존 배틀은 SQL로 초급을 채웠고, 어드민이 새로 만드는 배틀도 초급으로 저장된다. 어드민 배틀 목록에도 `difficulty`가 내려간다. | 상황 | 응답 | |---|---| diff --git a/docs/erd/class.puml b/docs/erd/class.puml index c5ebfc5e..872478c7 100644 --- a/docs/erd/class.puml +++ b/docs/erd/class.puml @@ -513,6 +513,7 @@ note bottom of battles → 에디터픽 → 참여자 수 → 최신 id 순, 최대 3개 (모자라면 채우지 않음) difficulty·category·class_recommendable 는 어드민 PATCH /admin/battles/{id}/class-settings 로 설정 추천을 켜려면 difficulty·category 필수, 커스텀 배틀(creator_type USER)은 설정 불가 + difficulty 는 임시로 초급 기본값 (#571: 기존 NULL → BEGINNER, 어드민 신규 생성도 BEGINNER) end note note bottom of classes diff --git a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java index 76bd7bd4..182f2379 100644 --- a/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java +++ b/src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java @@ -67,7 +67,8 @@ public BattleSimpleResponse toSimpleResponse(Battle battle) { urlProvider.getImageUrl(FileCategory.BATTLE, battle.getThumbnailUrl()), battle.getStatus() != null ? battle.getStatus().name() : "PENDING", battle.getPublishAt(), - battle.getCreatedAt() + battle.getCreatedAt(), + battle.getDifficulty() ); } diff --git a/src/main/java/com/swyp/picke/domain/battle/dto/response/BattleSimpleResponse.java b/src/main/java/com/swyp/picke/domain/battle/dto/response/BattleSimpleResponse.java index b0e653fa..9156a958 100644 --- a/src/main/java/com/swyp/picke/domain/battle/dto/response/BattleSimpleResponse.java +++ b/src/main/java/com/swyp/picke/domain/battle/dto/response/BattleSimpleResponse.java @@ -1,5 +1,6 @@ package com.swyp.picke.domain.battle.dto.response; +import com.swyp.picke.domain.battle.enums.BattleDifficulty; import java.time.LocalDateTime; public record BattleSimpleResponse( @@ -8,5 +9,6 @@ public record BattleSimpleResponse( String thumbnailUrl, String status, LocalDateTime publishAt, - LocalDateTime createdAt + LocalDateTime createdAt, + BattleDifficulty difficulty ) {} diff --git a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java index 72ec6a73..45c0088e 100644 --- a/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java @@ -13,6 +13,7 @@ import com.swyp.picke.domain.battle.entity.BattleOptionTag; import com.swyp.picke.domain.battle.entity.BattleTag; import com.swyp.picke.domain.battle.enums.BattleCreatorType; +import com.swyp.picke.domain.battle.enums.BattleDifficulty; import com.swyp.picke.domain.battle.enums.BattleStatus; import com.swyp.picke.domain.battle.util.BattleOptionDisplay; import com.swyp.picke.domain.user.dto.response.UserBattleStatusResponse; @@ -337,6 +338,8 @@ public AdminBattleDetailResponse createBattle(AdminBattleCreateRequest request, request.audioDuration(), request.status() ); + // 수준은 기획 확정 전까지 초급으로 두고, 어드민 상세에서 바꾼다 (#571) + battle.updateClassSettings(BattleDifficulty.BEGINNER, null, null, null); battle = battleRepository.save(battle); if (request.tagIds() != null) { diff --git a/src/main/resources/db/migration/V20261024_01__battles_difficulty_beginner.sql b/src/main/resources/db/migration/V20261024_01__battles_difficulty_beginner.sql new file mode 100644 index 00000000..545d36cf --- /dev/null +++ b/src/main/resources/db/migration/V20261024_01__battles_difficulty_beginner.sql @@ -0,0 +1,3 @@ +-- #571 수준이 비어 있는 배틀을 임시로 전부 초급으로 채운다 (어드민 상세에서 변경) +-- 멱등하게 작성되어 재실행해도 안전하다. +UPDATE battles SET difficulty = 'BEGINNER' WHERE difficulty IS NULL; diff --git a/src/main/resources/db/migration/V20261024_rollback__battles_difficulty_beginner.sql b/src/main/resources/db/migration/V20261024_rollback__battles_difficulty_beginner.sql new file mode 100644 index 00000000..fc253949 --- /dev/null +++ b/src/main/resources/db/migration/V20261024_rollback__battles_difficulty_beginner.sql @@ -0,0 +1,3 @@ +-- Rollback for V20261024_01__battles_difficulty_beginner.sql +-- 어떤 행을 채웠는지 따로 기록하지 않으므로, 어드민이 초급으로 직접 고른 배틀도 함께 비워진다. +UPDATE battles SET difficulty = NULL WHERE difficulty = 'BEGINNER' AND creator_type <> 'USER'; diff --git a/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java b/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java index 0998a24e..5f9ca9f4 100644 --- a/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java +++ b/src/test/java/com/swyp/picke/domain/admin/controller/AdminContentCreationIntegrationTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.swyp.picke.domain.battle.entity.Battle; +import com.swyp.picke.domain.battle.enums.BattleDifficulty; import com.swyp.picke.domain.battle.entity.BattleOption; import com.swyp.picke.domain.battle.repository.BattleOptionRepository; import com.swyp.picke.domain.battle.repository.BattleOptionTagRepository; @@ -149,6 +150,7 @@ void createBattle_persistsAllMappedFields() throws Exception { assertThat(savedBattle.getThumbnailUrl()).isEqualTo("images/battles/battle-thumb.png"); assertThat(savedBattle.getAudioDuration()).isEqualTo(95); assertThat(savedBattle.getTargetDate()).isEqualTo(targetDate); + assertThat(savedBattle.getDifficulty()).isEqualTo(BattleDifficulty.BEGINNER); assertThat(options).hasSize(2); BattleOption optionA = options.stream().filter(option -> option.getDisplayOrder() == 1).findFirst().orElseThrow(); diff --git a/src/test/java/com/swyp/picke/domain/classroom/controller/ClassBattleRecommendationApiIntegrationTest.java b/src/test/java/com/swyp/picke/domain/classroom/controller/ClassBattleRecommendationApiIntegrationTest.java index 3b7c1dfb..05ac3934 100644 --- a/src/test/java/com/swyp/picke/domain/classroom/controller/ClassBattleRecommendationApiIntegrationTest.java +++ b/src/test/java/com/swyp/picke/domain/classroom/controller/ClassBattleRecommendationApiIntegrationTest.java @@ -157,6 +157,18 @@ void adminClassSettings() throws Exception { JsonNode items = data(recommend(Map.of("difficulty", "ADVANCED", "categories", "역사"))).get("items"); assertThat(ids(items)).containsExactly(battle.getId()); + + JsonNode adminItems = data(mockMvc.perform(get("/api/v1/admin/battles") + .header("Authorization", "Bearer " + adminToken) + .param("size", "100"))).get("items"); + JsonNode listed = null; + for (JsonNode item : adminItems) { + if (item.get("battleId").asLong() == battle.getId()) { + listed = item; + } + } + assertThat(listed).isNotNull(); + assertThat(listed.get("difficulty").asText()).isEqualTo("ADVANCED"); } @Test