Skip to content

20260422 #27 행성 지역 해금 도메인 구현#42

Merged
EM-H20 merged 16 commits into
mainfrom
20260422_#27_행성_지역_해금_도메인_구현
May 29, 2026

Hidden character warning

The head ref may contain hidden characters: "20260422_#27_\ud589\uc131_\uc9c0\uc5ed_\ud574\uae08_\ub3c4\uba54\uc778_\uad6c\ud604"
Merged

20260422 #27 행성 지역 해금 도메인 구현#42
EM-H20 merged 16 commits into
mainfrom
20260422_#27_행성_지역_해금_도메인_구현

Conversation

@EM-H20

@EM-H20 EM-H20 commented May 29, 2026

Copy link
Copy Markdown
Contributor

✨ 변경 사항


✅ 테스트


  • 수동 테스트 완료
  • 테스트 코드 완료

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 우주 탐험 시스템 추가: 행성 및 지역 탐험 기능 제공
    • 탐험 노드 관리: 행성과 지역을 단계적으로 해금 가능
    • 진행도 추적: 사용자별 탐험 진행 상태 및 해금 이력 저장
    • 연료 시스템 연계: 행성/지역 해금 시 연료 소모
    • 선행 조건 게이트: 행성 해금 전 선행 행성 클리어 필요

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Walkthrough

탐험(Exploration) 도메인을 재구현하여 행성과 지역의 계층적 노드 관리, 사용자별 해금 진행도 추적, 연료 소모를 통한 해금 시스템을 구현했습니다. 공통 예외 인프라, JPA 엔티티/저장소, 비즈니스 로직 서비스, REST API, Flyway 마이그레이션, 종합 테스트 및 API 문서를 포함합니다.

Changes

Exploration Domain - Complete Implementation

Layer / File(s) Summary
Exception Infrastructure for Exploration
SS-Common/build.gradle, SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/ErrorCode.java, ErrorResponse.java, InsufficientFuelException.java, GlobalExceptionHandler.java, SS-Common/src/test/java/.../ErrorResponseTest.java
ErrorCode에 PLANET_NOT_FOUND, REGION_NOT_FOUND, ALREADY_UNLOCKED, PLANET_LOCKED, PREREQUISITE_NOT_CLEARED 5개 상수 추가. ErrorResponse 레코드에 nullable Integer requiredFuel/currentFuel 필드와 @JsonInclude(NON_NULL) 직렬화 정책 추가. InsufficientFuelException 신규 예외 클래스 및 GlobalExceptionHandler.handleInsufficientFuel 메서드 추가. Spring Boot Flyway 의존성 추가. 직렬화 및 팩토리 메서드 동작 테스트.
Node Type Definition and JPA Conversion
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/constant/NodeType.java, NodeTypeConverter.java
NodeType enum으로 PLANET/REGION 구분. value() 메서드로 소문자 직렬화, from(String) 정적 메서드로 역변환. NodeTypeConverter implements AttributeConverter<NodeType, String>로 JPA 칼럼 변환.
Exploration Node and User Progress Entities
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/entity/ExplorationNode.java, UserExploration.java, SS-Study/src/test/java/.../ExplorationNodeTest.java, UserExplorationTest.java
ExplorationNode: exploration_nodes 테이블 매핑, parentId/prerequisiteNodeId 계층 관계, requiredFuel/sortOrder/icon 필드 포함. UserExploration: user_exploration_progress 테이블 매핑, (user_id, node_id) 유니크 제약, unlock 정적 팩토리 메서드. 빌더 매핑 및 팩토리 동작 검증.
Repository Interfaces and Test Configuration
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/repository/ExplorationNodeRepository.java, UserExplorationRepository.java, SS-Study/src/test/java/.../StudyTestApplication.java, ExplorationNodeRepositoryTest.java, UserExplorationRepositoryTest.java
ExplorationNodeRepository: findByNodeTypeOrderBySortOrderAsc, findByParentIdOrderBySortOrderAsc 쿼리 메서드. UserExplorationRepository: findByUserId, existsByUserIdAndNodeId. StudyTestApplication @EnableJpaRepositories에 exploration 리포지토리 패키지 등록. 정렬 순서 및 UNIQUE 제약 검증 테스트.
Response DTOs with UTC Formatting
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/ProgressDto.java, PlanetResponse.java, RegionResponse.java, UnlockedNodeDto.java, RegionUnlockResponse.java, PlanetUnlockResponse.java
ProgressDto: clearedChildren/totalChildren/progressRatio. PlanetResponse/RegionResponse: 노드 메타데이터 + progress/해금 상태 + 지도 좌표. UnlockedNodeDto: 해금된 노드 요약(id/name/isUnlocked/isCleared/unlockedAt UTC). RegionUnlockResponse/PlanetUnlockResponse: 해금 후 연료 소모량/잔액/클리어 여부. 모두 UTC ISO-8601 formatUtc 헬퍼 포함.
Exploration Service: Queries and Unlock Operations
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java
getPlanets(userId): 행성 목록 + 자식 지역 클리어/총 개수 + 진행률 파생. getRegions(userId, planetId): 특정 행성의 지역 목록 + 해금 여부. unlockRegion(userId, regionId): 부모 잠금 검증 → 연료 부족 검증 → 소모 → 저장 → planetCleared 판정. unlockPlanet(userId, planetId): 선행 조건 검증 추가 + 연료 처리. 헬퍼: progressMap 생성, requireFuel 검증, isPlanetCleared 계산.
Service Unit Tests with Mockito
SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/service/ExplorationServiceTest.java
getPlanets 파생 로직 검증. getRegions 행성 미존재 예외 검증. unlockRegion 성공/연료부족/부모잠금/중복해금/미존재 5가지 시나리오. unlockPlanet 성공/선행미클리어/연료부족/중복해금/미존재 5가지 시나리오. FuelService 모킹 및 consume 호출 검증.
REST Controller with 4 Endpoints
SS-Web/src/main/java/com/elipair/spacestudyship/controller/exploration/ExplorationController.java
GET /api/explorations/planets: 행성 목록 조회. GET /api/explorations/planets/{planetId}/regions: 지역 목록 조회. POST /api/explorations/regions/{regionId}/unlock: 지역 해금. POST /api/explorations/planets/{planetId}/unlock: 행성 해금. @AuthMember로 사용자 주입, @Operation으로 에러 케이스 명시.
Controller Tests with MockMvc
SS-Web/src/test/java/com/elipair/spacestudyship/controller/exploration/ExplorationControllerTest.java
4개 엔드포인트 정상 응답 검증. PLANET_NOT_FOUND 404 에러. INSUFFICIENT_FUEL 400 with requiredFuel/currentFuel 본문. PREREQUISITE_NOT_CLEARED 400 에러. LoginMember 핸들러 및 GlobalExceptionHandler 연동.
Flyway Migration Schema and Seed Data
SS-Web/src/main/resources/db/migration/V0_0_42__add_exploration.sql, application.yml, CLAUDE.md
exploration_nodes 테이블: id/name/nodeType/depth/icon/parentId/prerequisiteNodeId/requiredFuel/sortOrder/description/mapX/mapY + 제약/인덱스. user_exploration_progress 테이블: userId/nodeId/isUnlocked/isCleared/unlockedAt + UNIQUE(user_id, node_id) + 인덱스. 행성 8개 + 지역 30개 시드 삽입. baseline-version: 0 설정. 마이그레이션 이력 기록.
API Specs and Requirements Documentation
docs/api-specs/05_exploration.md, exploration-frontend-requirements.md, docs/superpowers/plans/2026-05-29-exploration-domain.md, docs/superpowers/specs/2026-05-29-exploration-domain-design.md
05_exploration.md: prerequisiteId 필드/행성 해금 조건 추가, PREREQUISITE_NOT_CLEARED/INSUFFICIENT_FUEL 에러 예시. exploration-frontend-requirements.md: JWT 인증/프론트 필드/엔드포인트 호출/에러 코드/응답 스키마 정의. 구현 계획: Task 0~12 상세 명세. 설계 문서: 전체 아키텍처/로직/스키마/시드 규칙.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

이 PR은 새로운 도메인의 전체 스택(예외, 엔티티, 저장소, 서비스, DTO, 컨트롤러, 마이그레이션, 테스트, 문서)을 포함하는 주요 기능 구현입니다. 서비스 로직의 연료 차감/해금 흐름, 선행 조건 게이트, 트랜잭션 원자성, 복잡한 테스트 시나리오(Mock 상호작용)로 인해 높은 검토 난이도를 요구합니다. 다만 일관된 패턴과 명확한 의도로 인해 극도로 복잡하지는 않습니다.

Possibly Related Issues

Possibly Related PRs

  • SpaceStudyShip/SpaceStudyShip-BE#35: 본 PR의 InsufficientFuelException과 ErrorCode.INSUFFICIENT_FUEL 처리가 선행 PR의 INSUFFICIENT_FUEL 에러 코드 정의에 직접 의존합니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '행성 지역 해금 도메인 구현'이라는 주요 변경 사항을 명확하게 요약하고 있으며, 추가된 대부분의 파일들(ExplorationNode, UserExploration, ExplorationService, ExplorationController 등)과 Flyway 마이그레이션이 모두 이 목표에 부합합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 20260422_#27_행성_지역_해금_도메인_구현

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (6)
docs/api-specs/05_exploration.md (1)

395-405: ⚡ Quick win

코드 블록 언어를 명시해 MD040 경고를 제거해주세요.

현재 처리 로직 fenced block에 언어 태그가 없어 markdownlint(MD040) 경고가 발생합니다. ```text 또는 ```sql처럼 명시해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api-specs/05_exploration.md` around lines 395 - 405, The fenced code
block containing the SQL-like transaction (starts with "BEGIN TRANSACTION;") is
missing a language tag which triggers markdownlint MD040; update that fenced
block to include a language identifier such as ```sql (or ```text) so the block
becomes ```sql ... ``` and the linter warning is resolved.
docs/superpowers/specs/2026-05-29-exploration-domain-design.md (1)

53-69: ⚡ Quick win

마크다운 린트 경고(MD040/MD028/MD058)를 정리해주세요.

코드펜스 언어 태그 누락, blockquote 내부 공백 줄, 테이블 주변 공백 누락이 섞여 있습니다. 문서 CI 안정성을 위해 한 번에 정리하는 걸 권장합니다.

Also applies to: 125-137, 140-145, 148-157, 160-167, 169-171, 279-283, 295-298

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-29-exploration-domain-design.md` around lines
53 - 69, Fix the Markdown lint issues by adding missing fence language tags to
all code blocks (e.g., the directory tree block should be ```text or ```bash),
removing extra blank lines inside any blockquotes so there's no leading/trailing
empty lines within ">" blocks, and ensuring there is a blank line before and
after every table and code fence; apply these fixes consistently across the
mentioned sections (the directory tree/code blocks, blockquotes, and tables) so
MD040/MD028/MD058 warnings are resolved.
docs/superpowers/plans/2026-05-29-exploration-domain.md (1)

1861-1863: ⚡ Quick win

코드 펜스에 언어 태그를 추가해 markdownlint 경고를 없애주세요.

여러 fenced block이 언어 미지정이라 MD040 경고가 발생합니다.

Also applies to: 1883-1885, 1893-1895, 1899-1901, 1905-1907

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md` around lines 1861 -
1863, Add language tags to the fenced code blocks that currently begin with ```
to silence markdownlint MD040; specifically update the fences that contain the
table rows like the one referencing `V0_0_42__add_exploration.sql` and similar
blocks (those listing migration/version rows) by changing ``` to ```sql or
```text as appropriate so the code fence includes a language identifier; apply
the same change to the other occurrences that list migration rows (the blocks
noted after the first, and the ones around the other migration entries) so all
fenced blocks have a language tag.
SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/InsufficientFuelException.java (1)

6-6: 💤 Low value

CustomException 패턴과의 일관성을 고려해보세요.

현재 구현은 RuntimeException을 직접 상속하고 있으나, 코딩 가이드라인에서는 CustomExceptionErrorCode enum을 사용한 에러 처리를 권장합니다. 현재 구현은 추가 데이터(requiredFuel, currentFuel)를 전달하기 위한 합리적인 설계 선택이지만, 프로젝트 전체의 예외 처리 일관성을 위해 다음 옵션을 고려해볼 수 있습니다:

  1. CustomException을 확장하여 옵셔널 데이터 필드를 지원하도록 개선
  2. InsufficientFuelExceptionCustomException을 상속하도록 변경

현 상태로도 기능적으로 문제는 없으며, 전용 @ExceptionHandler를 통해 적절히 처리되고 있습니다.

코딩 가이드라인에 따르면 CustomException과 ErrorCode enum을 사용한 에러 처리가 권장됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/InsufficientFuelException.java`
at line 6, InsufficientFuelException currently extends RuntimeException but
should follow the project pattern by extending CustomException and using the
ErrorCode enum; update InsufficientFuelException to extend CustomException, call
super(ErrorCode.INSUFFICIENT_FUEL) (or the appropriate enum constant) from its
constructors, retain the requiredFuel and currentFuel fields and their getters,
and ensure constructors forward a clear message to the CustomException base (or
set message via ErrorCode) so existing `@ExceptionHandler` logic continues to
work; alternatively, if you prefer keeping RuntimeException, add support in
CustomException to accept optional extra-data fields and adapt
InsufficientFuelException to populate those, referencing the class name
InsufficientFuelException, fields requiredFuel/currentFuel, and the ErrorCode
enum.
SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/repository/UserExplorationRepositoryTest.java (1)

37-39: ⚡ Quick win

유니크 제약 위반 시 더 구체적인 예외 타입을 검증하세요.

현재 Exception.class는 너무 광범위하여 의도하지 않은 다른 예외도 테스트를 통과시킬 수 있습니다. 유니크 제약 위반 시 발생하는 DataIntegrityViolationException을 명시적으로 검증하면 테스트의 정확성이 높아집니다.

♻️ 제안하는 개선안
+import org.springframework.dao.DataIntegrityViolationException;

 assertThatThrownBy(() ->
         repository.saveAndFlush(UserExploration.unlock(1L, "mars", false)))
-        .isInstanceOf(Exception.class);
+        .isInstanceOf(DataIntegrityViolationException.class);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/repository/UserExplorationRepositoryTest.java`
around lines 37 - 39, The test currently asserts a too-broad exception when
calling repository.saveAndFlush(UserExploration.unlock(1L, "mars", false)));
replace the generic isInstanceOf(Exception.class) with
isInstanceOf(DataIntegrityViolationException.class) to explicitly verify the
unique constraint violation, and add the necessary import for
org.springframework.dao.DataIntegrityViolationException in
UserExplorationRepositoryTest.java; keep the assertThatThrownBy usage and target
the same repository.saveAndFlush / UserExploration.unlock invocation.
SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java (1)

107-109: ⚡ Quick win

연료 소모 시 결정적 멱등 키 사용을 권장합니다.

fuelService.consumetransactionId 기반 멱등성을 지원하지만, 매 호출마다 UUID.randomUUID()를 전달하면 그 보호가 무력화됩니다. 현재는 existsByUserIdAndNodeId 선검사와 유니크 제약으로 중복 소모가 방지되나, 재시도 안전성과 의도 명확화를 위해 exploration-unlock-{userId}-{nodeId} 같은 결정적 키를 권장합니다. (unlockPlanet의 Line 141-143도 동일)

♻️ 제안
         FuelTransactionResponse fuelTx = fuelService.consume(
                 userId, region.getRequiredFuel(), FuelReason.EXPLORATION_UNLOCK,
-                regionId, UUID.randomUUID().toString());
+                regionId, "exploration-unlock-" + userId + "-" + regionId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java`
around lines 107 - 109, The current calls to fuelService.consume in
ExplorationService use UUID.randomUUID() which defeats the service's
transactionId-based idempotency; replace the random id with a deterministic id
(e.g. "exploration-unlock-{userId}-{regionId}") when calling fuelService.consume
for the unlock flow (the call using userId, region.getRequiredFuel(),
FuelReason.EXPLORATION_UNLOCK, regionId, UUID.randomUUID().toString()), and
apply the same change in unlockPlanet where UUID.randomUUID() is used, so
transactionId is stable across retries and expresses intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/api-specs/exploration-frontend-requirements.md`:
- Line 6: Replace any local absolute paths like "/Users/..." in
docs/api-specs/exploration-frontend-requirements.md with repository-relative
paths or simple filenames so the doc is portable; specifically update references
pointing at lib/features/exploration/ (and any code blocks or examples) to use
relative paths (e.g., lib/features/exploration/ComponentName or
./lib/features/exploration/...) or just the file names, and ensure any copied
examples no longer contain user-specific absolute paths.
- Around line 204-212: The backend-spec column in the comparison table is out of
sync with the canonical contract in 05_exploration.md; update the rows "행성 구성",
"진행 게이트", "행성 연료", "지구 지역", "지역 ID 규칙", and "지구 지역 연료" in the table under
docs/api-specs/exploration-frontend-requirements.md so the "백엔드 spec 예시" values
exactly match the latest 05_exploration.md contract: remove the moon/other stale
planets if absent in the canonical spec, make the prerequisite/chain semantics
match (e.g., use `prerequisiteId` chain behavior if present), set planet fuel
numbers to the canonical values, align earth-region list/count to the canonical
regions, switch region ID format to the canonical naming scheme (name-based vs
prefix-based) and update earth-region fuel ranges to the values from
05_exploration.md.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md`:
- Line 12: Replace the hard-coded local absolute path string
"/Users/luca/workspace/Flutter_Project/space_study_ship/lib/features/exploration/data/seed/exploration_seed_data.dart"
with a repository-relative reference (e.g., a path relative to the repo root or
a docs asset link) so the document uses a portable, team-sharable path; update
the mention in the markdown so it points to the same file via repo-relative
notation (or a relative link) rather than the developer's local filesystem.
- Around line 222-227: Replace direct, auto-executing commit instructions (e.g.,
the "Step 8: Commit" block that runs `git add ...` and `git commit -m "..."`)
with a prompt that requires explicit user approval before committing; update the
text so the commit step reads something like "Prepare commit (do not run git
commit automatically) — ask user to confirm before executing `git commit`" and
apply the same change to all other commit sections referenced (the blocks at the
other occurrences listed in the comment). Locate the commit blocks by searching
for the "Step *: Commit" headings and the inline git commands (e.g., the `git
add ...` and `git commit -m` lines) and replace the imperative commit action
with a clear instruction to request user confirmation prior to running the
commit command.
- Line 17: Update the commit message example line that currently reads "커밋 형식:
`탐험 도메인 구현 : {type} : {설명} `#27``" to match the repository standard by using the
format "{issue_title} : {type} : {change_description} {issue_url}" and enforce
the allowed types (feat, fix, refactor, docs, chore, test, style) and the rule
forbidding emojis and special characters; replace the hardcoded "`#27`" style
issue marker with the full {issue_url} placeholder and ensure the example uses
the canonical order and placeholders described in the repository rule.

In `@docs/superpowers/specs/2026-05-29-exploration-domain-design.md`:
- Line 6: Remove the personal absolute path string
`/Users/luca/workspace/Flutter_Project/space_study_ship/lib/features/exploration/data/seed/exploration_seed_data.dart`
from the document and replace it with a portable reference such as a
repository-relative path (e.g.,
`lib/features/exploration/data/seed/exploration_seed_data.dart`) or a generic
placeholder (`<project-root>/lib/...`) so the spec no longer exposes local
environment details; update any surrounding text to match the new, portable path
reference.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/PlanetResponse.java`:
- Around line 33-35: The formatter assumes the LocalDateTime is UTC which can
misrepresent UserExploration.unlockedAt (set via UserExploration.unlock() using
LocalDateTime.now()); update PlanetResponse.formatUtc(LocalDateTime) to convert
the local time to an Instant using the server timezone explicitly (e.g.
ZonedDateTime.of(time, ZoneId.systemDefault()).toInstant()) before formatting
with ISO_UTC, or alternatively change UserExploration.unlock() to store an
Instant (Instant.now(ZoneOffset.UTC)) and have formatUtc accept/format an
Instant; pick one approach and apply it consistently across formatUtc,
UserExploration.unlock(), and the unlockedAt field handling.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/entity/UserExploration.java`:
- Around line 47-55: The unlock method records unlockedAt using
LocalDateTime.now() (system default zone) while DTO.formatUtc(LocalDateTime)
treats the value as UTC, causing timezone offsets; modify UserExploration.unlock
to set unlockedAt in UTC (e.g., LocalDateTime.now(ZoneOffset.UTC) or
LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC)) so the stored unlockedAt
aligns with the DTO's UTC serialization expectations (update the unlock method
and ensure the unlockedAt field assignment uses the UTC-based LocalDateTime).

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java`:
- Around line 47-62: clearedByParent currently counts only regions present in
the progress/unlocked set and thus omits free regions (requiredFuel==0); update
the aggregation so free regions are treated as cleared too by changing the
filter used to build clearedByParent to include regions where
r.getRequiredFuel() == 0 in addition to unlocked.contains(r.getId()) (i.e.,
count regions where unlocked contains the id OR requiredFuel == 0), so the rest
of the planets.stream(...) logic (PlanetResponse.of, isCleared, ratio
calculations) reflects the same implicit unlocking used by getRegions.
- Around line 161-169: The isPlanetCleared method wrongly requires a
UserExploration record for every region, causing planets with free regions
(requiredFuel==0) to never be considered cleared; update isPlanetCleared to
treat a region as cleared if either the user has a UserExploration record
(userExplorationRepository → UserExploration nodeId) OR the region reports
requiredFuel==0 (check ExplorationNode.getRequiredFuel() == 0), so use
regions.stream().allMatch(r -> r.getRequiredFuel() == 0 ||
unlocked.contains(r.getId())); also review related logic in getRegions,
unlockRegion, and unlockPlanet to ensure the same implicit “free regions are
already unlocked/cleared” semantics are applied consistently.

In
`@SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/service/ExplorationServiceTest.java`:
- Around line 218-221: The test fixture incorrectly adds a UserExploration
record for the free region "korea" (requiredFuel=0), masking the real behavior
where free regions do not have progress records and unlockRegion would throw
ALREADY_UNLOCKED; update the mocks so
nodeRepository.findByParentIdOrderBySortOrderAsc still returns region("korea",
"earth", 0, 0) but userExplorationRepository.findByUserId does NOT include
UserExploration.unlock(1L, "korea", true) (return empty or omit that region),
and adjust any related expectations/assertions so the service exercises the code
path that treats free regions as absent from the progress map (affecting
isPlanetCleared/unlockRegion behavior).

In
`@SS-Web/src/main/java/com/elipair/spacestudyship/controller/exploration/ExplorationController.java`:
- Around line 53-71: Both unlockRegion and unlockPlanet are creation actions but
currently return 200; change their responses to 201. Replace the
ResponseEntity.ok(...) uses in ExplorationController.unlockRegion and
ExplorationController.unlockPlanet with
ResponseEntity.status(HttpStatus.CREATED).body(...) (calling
explorationService.unlockRegion(...) / unlockPlanet(...) as before). If the API
spec or frontend contract remains 200, leave as-is; otherwise update
docs/api-specs/05_exploration.md and adjust the test assertion in
ExplorationControllerTest (the isOk() check) to expect CREATED.

---

Nitpick comments:
In `@docs/api-specs/05_exploration.md`:
- Around line 395-405: The fenced code block containing the SQL-like transaction
(starts with "BEGIN TRANSACTION;") is missing a language tag which triggers
markdownlint MD040; update that fenced block to include a language identifier
such as ```sql (or ```text) so the block becomes ```sql ... ``` and the linter
warning is resolved.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md`:
- Around line 1861-1863: Add language tags to the fenced code blocks that
currently begin with ``` to silence markdownlint MD040; specifically update the
fences that contain the table rows like the one referencing
`V0_0_42__add_exploration.sql` and similar blocks (those listing
migration/version rows) by changing ``` to ```sql or ```text as appropriate so
the code fence includes a language identifier; apply the same change to the
other occurrences that list migration rows (the blocks noted after the first,
and the ones around the other migration entries) so all fenced blocks have a
language tag.

In `@docs/superpowers/specs/2026-05-29-exploration-domain-design.md`:
- Around line 53-69: Fix the Markdown lint issues by adding missing fence
language tags to all code blocks (e.g., the directory tree block should be
```text or ```bash), removing extra blank lines inside any blockquotes so
there's no leading/trailing empty lines within ">" blocks, and ensuring there is
a blank line before and after every table and code fence; apply these fixes
consistently across the mentioned sections (the directory tree/code blocks,
blockquotes, and tables) so MD040/MD028/MD058 warnings are resolved.

In
`@SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/InsufficientFuelException.java`:
- Line 6: InsufficientFuelException currently extends RuntimeException but
should follow the project pattern by extending CustomException and using the
ErrorCode enum; update InsufficientFuelException to extend CustomException, call
super(ErrorCode.INSUFFICIENT_FUEL) (or the appropriate enum constant) from its
constructors, retain the requiredFuel and currentFuel fields and their getters,
and ensure constructors forward a clear message to the CustomException base (or
set message via ErrorCode) so existing `@ExceptionHandler` logic continues to
work; alternatively, if you prefer keeping RuntimeException, add support in
CustomException to accept optional extra-data fields and adapt
InsufficientFuelException to populate those, referencing the class name
InsufficientFuelException, fields requiredFuel/currentFuel, and the ErrorCode
enum.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java`:
- Around line 107-109: The current calls to fuelService.consume in
ExplorationService use UUID.randomUUID() which defeats the service's
transactionId-based idempotency; replace the random id with a deterministic id
(e.g. "exploration-unlock-{userId}-{regionId}") when calling fuelService.consume
for the unlock flow (the call using userId, region.getRequiredFuel(),
FuelReason.EXPLORATION_UNLOCK, regionId, UUID.randomUUID().toString()), and
apply the same change in unlockPlanet where UUID.randomUUID() is used, so
transactionId is stable across retries and expresses intent.

In
`@SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/repository/UserExplorationRepositoryTest.java`:
- Around line 37-39: The test currently asserts a too-broad exception when
calling repository.saveAndFlush(UserExploration.unlock(1L, "mars", false)));
replace the generic isInstanceOf(Exception.class) with
isInstanceOf(DataIntegrityViolationException.class) to explicitly verify the
unique constraint violation, and add the necessary import for
org.springframework.dao.DataIntegrityViolationException in
UserExplorationRepositoryTest.java; keep the assertThatThrownBy usage and target
the same repository.saveAndFlush / UserExploration.unlock invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc9085e1-d694-4d5f-9b4e-4bd01f556ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 105419d and 00084e0.

📒 Files selected for processing (34)
  • CLAUDE.md
  • SS-Common/build.gradle
  • SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/ErrorCode.java
  • SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/ErrorResponse.java
  • SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/GlobalExceptionHandler.java
  • SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/InsufficientFuelException.java
  • SS-Common/src/test/java/com/elipair/spacestudyship/common/exception/ErrorResponseTest.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/constant/NodeType.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/constant/NodeTypeConverter.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/PlanetResponse.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/PlanetUnlockResponse.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/ProgressDto.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/RegionResponse.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/RegionUnlockResponse.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/dto/UnlockedNodeDto.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/entity/ExplorationNode.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/entity/UserExploration.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/repository/ExplorationNodeRepository.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/repository/UserExplorationRepository.java
  • SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/StudyTestApplication.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/entity/ExplorationNodeTest.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/entity/UserExplorationTest.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/repository/ExplorationNodeRepositoryTest.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/repository/UserExplorationRepositoryTest.java
  • SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/service/ExplorationServiceTest.java
  • SS-Web/src/main/java/com/elipair/spacestudyship/controller/exploration/ExplorationController.java
  • SS-Web/src/main/resources/application.yml
  • SS-Web/src/main/resources/db/migration/V0_0_42__add_exploration.sql
  • SS-Web/src/test/java/com/elipair/spacestudyship/controller/exploration/ExplorationControllerTest.java
  • docs/api-specs/05_exploration.md
  • docs/api-specs/exploration-frontend-requirements.md
  • docs/superpowers/plans/2026-05-29-exploration-domain.md
  • docs/superpowers/specs/2026-05-29-exploration-domain-design.md

> **작성:** 2026-05-29
> **대상 기능:** Exploration (행성/지역 탐험)
> **성격:** Flutter(프론트)가 백엔드에 요구하는 API 계약 명세. 백엔드 `docs/api-specs/05_exploration.md`와 대조·정합을 맞추기 위한 문서.
> **관련 코드:** `lib/features/exploration/`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

로컬 절대 경로는 저장소 상대 경로로 바꿔주세요.

/Users/... 형태 경로는 문서 이식성과 공유성을 떨어뜨립니다. 저장소 기준 상대 경로나 파일명 기준 참조로 변경하는 게 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api-specs/exploration-frontend-requirements.md` at line 6, Replace any
local absolute paths like "/Users/..." in
docs/api-specs/exploration-frontend-requirements.md with repository-relative
paths or simple filenames so the doc is portable; specifically update references
pointing at lib/features/exploration/ (and any code blocks or examples) to use
relative paths (e.g., lib/features/exploration/ComponentName or
./lib/features/exploration/...) or just the file names, and ensure any copied
examples no longer contain user-specific absolute paths.

Comment on lines +204 to +212
| 항목 | 프론트 시드 (게스트 로컬) | 백엔드 spec 예시 |
|------|--------------------------|------------------|
| 행성 구성 | 지구·수성·금성·화성·목성·토성·천왕성·해왕성 (8개, **달 없음**) | 지구·달·화성 (예시 3개) |
| 진행 게이트 | **선행조건 없음** (연료만 있으면 해금) | 선행 체인 (지구→달→화성, `prerequisiteId`) |
| 행성 연료 | earth 0 / mercury 3 / venus 5 / mars 10 / jupiter 20 / saturn 30 / uranus 45 / neptune 60 | earth 0 / moon 8 / mars 12 |
| 지구 지역 | 12개 (korea, japan, thailand, china, india, uk, france, canada, usa, brazil, australia, egypt) | 2개 예시 (대한민국, 일본) |
| 지역 ID 규칙 | `korea`, `japan`, `usa` (이름 기반) | `region-kr`, `region-jp` (prefix 기반) |
| 지구 지역 연료 | 0~3 | 4~6 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

백엔드 스펙 비교표가 현재 스펙과 상충합니다.

이 표의 백엔드 spec 예시 값(달 포함, region-kr, 연료 수치 등)은 같은 PR에서 갱신된 docs/api-specs/05_exploration.md와 충돌합니다. 최신 계약 기준으로 표를 맞추지 않으면 프론트 구현 판단이 뒤틀릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api-specs/exploration-frontend-requirements.md` around lines 204 - 212,
The backend-spec column in the comparison table is out of sync with the
canonical contract in 05_exploration.md; update the rows "행성 구성", "진행 게이트", "행성
연료", "지구 지역", "지역 ID 규칙", and "지구 지역 연료" in the table under
docs/api-specs/exploration-frontend-requirements.md so the "백엔드 spec 예시" values
exactly match the latest 05_exploration.md contract: remove the moon/other stale
planets if absent in the canonical spec, make the prerequisite/chain semantics
match (e.g., use `prerequisiteId` chain behavior if present), set planet fuel
numbers to the canonical values, align earth-region list/count to the canonical
regions, switch region ID format to the canonical naming scheme (name-based vs
prefix-based) and update earth-region fuel ranges to the values from
05_exploration.md.

**Tech Stack:** Java 21, Spring Boot 4, Spring Data JPA, Lombok, JUnit5+Mockito+AssertJ, Testcontainers(Postgres), Flyway, springdoc.

**Spec:** `docs/superpowers/specs/2026-05-29-exploration-domain-design.md`
**프론트 시드 원본:** `/Users/luca/workspace/Flutter_Project/space_study_ship/lib/features/exploration/data/seed/exploration_seed_data.dart`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

로컬 절대 경로 대신 저장소 기준 참조를 사용해주세요.

개인 환경 경로(/Users/...)는 문서 재사용성과 팀 공용성에 불리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md` at line 12, Replace
the hard-coded local absolute path string
"/Users/luca/workspace/Flutter_Project/space_study_ship/lib/features/exploration/data/seed/exploration_seed_data.dart"
with a repository-relative reference (e.g., a path relative to the repo root or
a docs asset link) so the document uses a portable, team-sharable path; update
the mention in the markdown so it points to the same file via repo-relative
notation (or a relative link) rather than the developer's local filesystem.

**공통 규칙:**
- 테스트: `./gradlew :SS-Study:test`, `./gradlew :SS-Web:test`, `./gradlew :SS-Common:test`. 단일: `--tests "FQCN"`.
- 테스트 환경 = Testcontainers + `ddl-auto=create-drop` (엔티티가 스키마 생성, Flyway 비활성). `members` FK는 엔티티에 매핑하지 않음(마이그레이션에만 존재).
- 커밋 형식: `탐험 도메인 구현 : {type} : {설명} #27`. 이슈번호 #27. **이모지 금지. Co-Authored-By 금지.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

문서의 커밋 메시지 규칙이 저장소 규칙과 다릅니다.

여기 적힌 포맷(#27)은 학습된 저장소 규칙({issue_title} : {type} : {change_description} {issue_url})과 불일치합니다. 계획 문구를 저장소 표준으로 맞춰주세요.
Based on learnings: "All commit messages must follow the format: {issue_title} : {type} : {change_description} {issue_url}, where type is one of: feat, fix, refactor, docs, chore, test, style. Emoji and special characters are forbidden in commit messages."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md` at line 17, Update
the commit message example line that currently reads "커밋 형식: `탐험 도메인 구현 : {type}
: {설명} `#27``" to match the repository standard by using the format "{issue_title}
: {type} : {change_description} {issue_url}" and enforce the allowed types
(feat, fix, refactor, docs, chore, test, style) and the rule forbidding emojis
and special characters; replace the hardcoded "`#27`" style issue marker with the
full {issue_url} placeholder and ensure the example uses the canonical order and
placeholders described in the repository rule.

Comment on lines +222 to +227
- [ ] **Step 8: Commit**

```bash
git add SS-Common/src/main/java/com/elipair/spacestudyship/common/exception/ SS-Common/src/test/java/com/elipair/spacestudyship/common/exception/ErrorResponseTest.java
git commit -m "탐험 도메인 구현 : feat : 탐험 ErrorCode 5종 + INSUFFICIENT_FUEL 응답 보강 #27"
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

명시적 사용자 요청 없이 커밋을 수행하도록 유도하는 단계는 정책과 충돌합니다.

여러 Task에서 즉시 git commit 실행을 지시하고 있어, 사용자 확인 선행 정책과 충돌합니다. 커밋 단계는 “사용자 승인 후 실행”으로 바꿔주세요.
Based on learnings: "Never commit without explicit user request. Claude must always ask for confirmation before creating any commits."

Also applies to: 287-290, 408-411, 522-525, 709-711, 898-900, 1121-1123, 1324-1326, 1474-1476, 1751-1753, 1868-1870, 1927-1929

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-29-exploration-domain.md` around lines 222 -
227, Replace direct, auto-executing commit instructions (e.g., the "Step 8:
Commit" block that runs `git add ...` and `git commit -m "..."`) with a prompt
that requires explicit user approval before committing; update the text so the
commit step reads something like "Prepare commit (do not run git commit
automatically) — ask user to confirm before executing `git commit`" and apply
the same change to all other commit sections referenced (the blocks at the other
occurrences listed in the comment). Locate the commit blocks by searching for
the "Step *: Commit" headings and the inline git commands (e.g., the `git add
...` and `git commit -m` lines) and replace the imperative commit action with a
clear instruction to request user confirmation prior to running the commit
command.

Comment on lines +47 to +55
public static UserExploration unlock(Long userId, String nodeId, boolean cleared) {
return UserExploration.builder()
.userId(userId)
.nodeId(nodeId)
.isUnlocked(true)
.isCleared(cleared)
.unlockedAt(LocalDateTime.now())
.build();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# formatUtc 구현 및 unlockedAt 직렬화 방식 확인
rg -nP -C5 '\bformatUtc\b' --type=java
rg -nP -C3 'unlockedAt' --type=java -g '**/dto/**'

Repository: SpaceStudyShip/SpaceStudyShip-BE

Length of output: 18302


unlockedAt 저장/직렬화 시간대 불일치 가능성
DTO의 formatUtc(LocalDateTime)ISO_INSTANT에 대해 time.toInstant(ZoneOffset.UTC)로 변환해 “UTC로 가정”하는데, unlock()LocalDateTime.now()(시스템 기본 타임존 기준 벽시계)를 그대로 기록하고 있어 JVM 타임존이 UTC가 아닐 때 응답 시각이 오프셋될 수 있습니다.

🛠️ 제안 수정
+import java.time.ZoneOffset;
+
 public static UserExploration unlock(Long userId, String nodeId, boolean cleared) {
     return UserExploration.builder()
             .userId(userId)
             .nodeId(nodeId)
             .isUnlocked(true)
             .isCleared(cleared)
-            .unlockedAt(LocalDateTime.now())
+            .unlockedAt(LocalDateTime.now(ZoneOffset.UTC))
             .build();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static UserExploration unlock(Long userId, String nodeId, boolean cleared) {
return UserExploration.builder()
.userId(userId)
.nodeId(nodeId)
.isUnlocked(true)
.isCleared(cleared)
.unlockedAt(LocalDateTime.now())
.build();
}
import java.time.ZoneOffset;
public static UserExploration unlock(Long userId, String nodeId, boolean cleared) {
return UserExploration.builder()
.userId(userId)
.nodeId(nodeId)
.isUnlocked(true)
.isCleared(cleared)
.unlockedAt(LocalDateTime.now(ZoneOffset.UTC))
.build();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/entity/UserExploration.java`
around lines 47 - 55, The unlock method records unlockedAt using
LocalDateTime.now() (system default zone) while DTO.formatUtc(LocalDateTime)
treats the value as UTC, causing timezone offsets; modify UserExploration.unlock
to set unlockedAt in UTC (e.g., LocalDateTime.now(ZoneOffset.UTC) or
LocalDateTime.ofInstant(Instant.now(), ZoneOffset.UTC)) so the stored unlockedAt
aligns with the DTO's UTC serialization expectations (update the unlock method
and ensure the unlockedAt field assignment uses the UTC-based LocalDateTime).

Comment on lines +47 to +62
Map<String, Long> totalByParent = regions.stream()
.collect(Collectors.groupingBy(ExplorationNode::getParentId, Collectors.counting()));
Map<String, Long> clearedByParent = regions.stream()
.filter(r -> unlocked.contains(r.getId()))
.collect(Collectors.groupingBy(ExplorationNode::getParentId, Collectors.counting()));

return planets.stream().map(p -> {
int total = totalByParent.getOrDefault(p.getId(), 0L).intValue();
int cleared = clearedByParent.getOrDefault(p.getId(), 0L).intValue();
boolean isUnlocked = p.getRequiredFuel() == 0 || unlocked.contains(p.getId());
boolean isCleared = total > 0 && cleared == total;
double ratio = total == 0 ? 0.0 : (double) cleared / total;
LocalDateTime unlockedAt = progress.containsKey(p.getId())
? progress.get(p.getId()).getUnlockedAt() : null;
return PlanetResponse.of(p, isUnlocked, isCleared, cleared, total, ratio, unlockedAt);
}).toList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

진행도 집계에서 무료 지역이 누락되어 getRegions와 불일치합니다.

clearedByParent(Line 49-51)는 진행도 맵에 존재하는 지역만 카운트하지만, requiredFuel==0 지역은 진행도 레코드가 생성되지 않습니다(isPlanetCleared에 남긴 동일 원인). 따라서 무료 지역을 포함한 행성은 모든 지역이 해금된 상태여도 cleared/progressRatio/isCleared(Line 55-58)가 getRegions의 암묵 해금 결과와 어긋나게 됩니다. 무료 지역도 cleared로 포함하도록 보정해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java`
around lines 47 - 62, clearedByParent currently counts only regions present in
the progress/unlocked set and thus omits free regions (requiredFuel==0); update
the aggregation so free regions are treated as cleared too by changing the
filter used to build clearedByParent to include regions where
r.getRequiredFuel() == 0 in addition to unlocked.contains(r.getId()) (i.e.,
count regions where unlocked contains the id OR requiredFuel == 0), so the rest
of the planets.stream(...) logic (PlanetResponse.of, isCleared, ratio
calculations) reflects the same implicit unlocking used by getRegions.

Comment on lines +161 to +169
private boolean isPlanetCleared(Long userId, String planetId) {
List<ExplorationNode> regions = nodeRepository.findByParentIdOrderBySortOrderAsc(planetId);
if (regions.isEmpty()) {
return false;
}
Set<String> unlocked = userExplorationRepository.findByUserId(userId).stream()
.map(UserExploration::getNodeId).collect(Collectors.toSet());
return regions.stream().allMatch(r -> unlocked.contains(r.getId()));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

무료 지역(requiredFuel==0)으로 인해 행성 클리어가 영구히 false가 됩니다.

unlocked 집합은 진행도 레코드(UserExploration)가 있는 노드만 포함합니다. 그러나 requiredFuel==0 지역은 unlockRegion이 항상 ALREADY_UNLOCKED을 던지므로(Line 100-103) 진행도 레코드가 절대 생성되지 않습니다. 결과적으로 무료 지역을 하나라도 가진 행성은 allMatch가 결코 true가 되지 못해, unlockRegionplanetCleared 판정과 unlockPlanet의 선행 조건 게이트(Line 134-137)가 영구적으로 차단됩니다.

getRegions(Line 75,77)가 무료 지역을 암묵 해금/클리어로 취급하는 것과도 모순됩니다. 무료 지역을 해금/클리어로 간주하도록 보정이 필요합니다.

🐛 보정 예시
     private boolean isPlanetCleared(Long userId, String planetId) {
         List<ExplorationNode> regions = nodeRepository.findByParentIdOrderBySortOrderAsc(planetId);
         if (regions.isEmpty()) {
             return false;
         }
         Set<String> unlocked = userExplorationRepository.findByUserId(userId).stream()
                 .map(UserExploration::getNodeId).collect(Collectors.toSet());
-        return regions.stream().allMatch(r -> unlocked.contains(r.getId()));
+        return regions.stream()
+                .allMatch(r -> r.getRequiredFuel() == 0 || unlocked.contains(r.getId()));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private boolean isPlanetCleared(Long userId, String planetId) {
List<ExplorationNode> regions = nodeRepository.findByParentIdOrderBySortOrderAsc(planetId);
if (regions.isEmpty()) {
return false;
}
Set<String> unlocked = userExplorationRepository.findByUserId(userId).stream()
.map(UserExploration::getNodeId).collect(Collectors.toSet());
return regions.stream().allMatch(r -> unlocked.contains(r.getId()));
}
private boolean isPlanetCleared(Long userId, String planetId) {
List<ExplorationNode> regions = nodeRepository.findByParentIdOrderBySortOrderAsc(planetId);
if (regions.isEmpty()) {
return false;
}
Set<String> unlocked = userExplorationRepository.findByUserId(userId).stream()
.map(UserExploration::getNodeId).collect(Collectors.toSet());
return regions.stream()
.allMatch(r -> r.getRequiredFuel() == 0 || unlocked.contains(r.getId()));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/main/java/com/elipair/spacestudyship/study/exploration/service/ExplorationService.java`
around lines 161 - 169, The isPlanetCleared method wrongly requires a
UserExploration record for every region, causing planets with free regions
(requiredFuel==0) to never be considered cleared; update isPlanetCleared to
treat a region as cleared if either the user has a UserExploration record
(userExplorationRepository → UserExploration nodeId) OR the region reports
requiredFuel==0 (check ExplorationNode.getRequiredFuel() == 0), so use
regions.stream().allMatch(r -> r.getRequiredFuel() == 0 ||
unlocked.contains(r.getId())); also review related logic in getRegions,
unlockRegion, and unlockPlanet to ensure the same implicit “free regions are
already unlocked/cleared” semantics are applied consistently.

Comment on lines +218 to +221
given(nodeRepository.findByParentIdOrderBySortOrderAsc("earth"))
.willReturn(List.of(region("korea", "earth", 0, 0)));
given(userExplorationRepository.findByUserId(1L))
.willReturn(List.of(UserExploration.unlock(1L, "korea", true)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

테스트 픽스처가 무료 지역 관련 결함을 가립니다.

여기(및 Line 136-138)에서 korea(requiredFuel=0)를 진행도 맵에 직접 넣어 선행 클리어/planetCleared를 충족시킵니다. 그러나 실제로는 무료 지역에 진행도 레코드가 생성되지 않으므로(unlockRegionALREADY_UNLOCKED을 던짐), isPlanetCleared가 무료 지역을 클리어로 인식하지 못합니다. 서비스 로직 보정 후, 무료 지역이 진행도 맵에 없는 실제 상태를 반영하도록 테스트를 보완해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Study/src/test/java/com/elipair/spacestudyship/study/exploration/service/ExplorationServiceTest.java`
around lines 218 - 221, The test fixture incorrectly adds a UserExploration
record for the free region "korea" (requiredFuel=0), masking the real behavior
where free regions do not have progress records and unlockRegion would throw
ALREADY_UNLOCKED; update the mocks so
nodeRepository.findByParentIdOrderBySortOrderAsc still returns region("korea",
"earth", 0, 0) but userExplorationRepository.findByUserId does NOT include
UserExploration.unlock(1L, "korea", true) (return empty or omit that region),
and adjust any related expectations/assertions so the service exercises the code
path that treats free regions as absent from the progress map (affecting
isPlanetCleared/unlockRegion behavior).

Comment on lines +53 to +71
@PostMapping("/regions/{regionId}/unlock")
public ResponseEntity<RegionUnlockResponse> unlockRegion(
@AuthMember LoginMember loginMember,
@PathVariable String regionId) {
return ResponseEntity.ok(explorationService.unlockRegion(loginMember.memberId(), regionId));
}

@Operation(summary = "행성 해금",
description = """
연료를 소비하여 행성을 해금합니다. 선행 행성을 클리어해야 해금할 수 있습니다.

에러: 400 INSUFFICIENT_FUEL(requiredFuel/currentFuel 동봉) / ALREADY_UNLOCKED / PREREQUISITE_NOT_CLEARED, 404 PLANET_NOT_FOUND
""")
@PostMapping("/planets/{planetId}/unlock")
public ResponseEntity<PlanetUnlockResponse> unlockPlanet(
@AuthMember LoginMember loginMember,
@PathVariable String planetId) {
return ResponseEntity.ok(explorationService.unlockPlanet(loginMember.memberId(), planetId));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

해금 엔드포인트는 HttpStatus.CREATED(201) 반환을 검토하세요.

unlockRegion/unlockPlanet은 새 UserExploration 진행 레코드를 영속화하는 생성 동작입니다. 현재는 ResponseEntity.ok(200)로 응답합니다. 생성 엔드포인트는 201을 반환하도록 규정되어 있어 검토가 필요합니다. 단, docs/api-specs/05_exploration.md의 프론트엔드 계약이 200으로 고정되어 있다면 계약을 우선하고, 변경 시 ExplorationControllerTestisOk() 단언도 함께 갱신해야 합니다.

As per coding guidelines: "For creation endpoints (new member, etc.), use HttpStatus.CREATED."

♻️ 제안 변경 (계약이 201을 허용하는 경우)
-        return ResponseEntity.ok(explorationService.unlockRegion(loginMember.memberId(), regionId));
+        return ResponseEntity.status(HttpStatus.CREATED)
+                .body(explorationService.unlockRegion(loginMember.memberId(), regionId));
-        return ResponseEntity.ok(explorationService.unlockPlanet(loginMember.memberId(), planetId));
+        return ResponseEntity.status(HttpStatus.CREATED)
+                .body(explorationService.unlockPlanet(loginMember.memberId(), planetId));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@PostMapping("/regions/{regionId}/unlock")
public ResponseEntity<RegionUnlockResponse> unlockRegion(
@AuthMember LoginMember loginMember,
@PathVariable String regionId) {
return ResponseEntity.ok(explorationService.unlockRegion(loginMember.memberId(), regionId));
}
@Operation(summary = "행성 해금",
description = """
연료를 소비하여 행성을 해금합니다. 선행 행성을 클리어해야 해금할 있습니다.
에러: 400 INSUFFICIENT_FUEL(requiredFuel/currentFuel 동봉) / ALREADY_UNLOCKED / PREREQUISITE_NOT_CLEARED, 404 PLANET_NOT_FOUND
""")
@PostMapping("/planets/{planetId}/unlock")
public ResponseEntity<PlanetUnlockResponse> unlockPlanet(
@AuthMember LoginMember loginMember,
@PathVariable String planetId) {
return ResponseEntity.ok(explorationService.unlockPlanet(loginMember.memberId(), planetId));
}
`@PostMapping`("/regions/{regionId}/unlock")
public ResponseEntity<RegionUnlockResponse> unlockRegion(
`@AuthMember` LoginMember loginMember,
`@PathVariable` String regionId) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(explorationService.unlockRegion(loginMember.memberId(), regionId));
}
`@Operation`(summary = "행성 해금",
description = """
연료를 소비하여 행성을 해금합니다. 선행 행성을 클리어해야 해금할 있습니다.
에러: 400 INSUFFICIENT_FUEL(requiredFuel/currentFuel 동봉) / ALREADY_UNLOCKED / PREREQUISITE_NOT_CLEARED, 404 PLANET_NOT_FOUND
""")
`@PostMapping`("/planets/{planetId}/unlock")
public ResponseEntity<PlanetUnlockResponse> unlockPlanet(
`@AuthMember` LoginMember loginMember,
`@PathVariable` String planetId) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(explorationService.unlockPlanet(loginMember.memberId(), planetId));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@SS-Web/src/main/java/com/elipair/spacestudyship/controller/exploration/ExplorationController.java`
around lines 53 - 71, Both unlockRegion and unlockPlanet are creation actions
but currently return 200; change their responses to 201. Replace the
ResponseEntity.ok(...) uses in ExplorationController.unlockRegion and
ExplorationController.unlockPlanet with
ResponseEntity.status(HttpStatus.CREATED).body(...) (calling
explorationService.unlockRegion(...) / unlockPlanet(...) as before). If the API
spec or frontend contract remains 200, leave as-is; otherwise update
docs/api-specs/05_exploration.md and adjust the test assertion in
ExplorationControllerTest (the isOk() check) to expect CREATED.

@github-actions

Copy link
Copy Markdown

🗑️ Preview 환경 삭제 완료!

항목
컨테이너 spacestudyship-pr-42
상태 삭제됨

다시 배포하려면: @suh-lab server build


🤖 이 댓글은 PR/Issue Preview 시스템에 의해 자동 생성되었습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant