Skip to content

fix: [ALT-284] 업장 근무자·초대 결함 수정 — 만료 초대 제외·근무자 목록 기본 ACTIVATED·초대 실패 사유 - #102

Open
hodoon wants to merge 4 commits into
devfrom
fix/ALT-284
Open

hodoon wants to merge 4 commits into
devfrom
fix/ALT-284

Conversation

@hodoon

@hodoon hodoon commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

요약

QA 2026-08-26 통합 테스트 1주차에서 확인된 업장 근무자·초대 도메인 결함 3건(C-04, D-01, C-01)을 수정했습니다. 리뷰 과정에서 초대 발송 경쟁 조건(부분 유니크 인덱스 V15)과 리뷰 지적 10건을 함께 반영했습니다. 초대 실패 응답 data·내 초대 응답 status 형태가 바뀌는 Breaking change가 있습니다.

변경 내용

P0 — 만료된 초대가 목록에 남아 수락·거절이 모두 실패하고 재초대까지 막힘 (C-04)

  • 초대 만료 스케줄러가 없어 expiresAt이 지나도 DB status는 PENDING 그대로였고, 목록 조회·"이미 초대 대기 중" 판정이 status = PENDING만 봤습니다. 만료 카드가 계속 보이고, 누르면 수락·거절 모두 409, 사장님은 같은 번호를 재초대할 수 없었습니다.
  • BusinessInvitationQueryRepositoryImpl에 "PENDING·유효"/"PENDING·만료" 헬퍼 한 쌍(pendingAndValid / pendingAndExpired)을 두고 목록·건수·재초대 판정이 모두 이걸 씁니다. 만료 기준 시각 now는 UseCase가 한 번 구해 리포지토리에 넘기므로 count와 list가 어긋나지 않고, 엔티티 isExpired()도 같은 경계(expiresAt <= now)를 씁니다.
  • 목록 규칙: 필터 없음 → 만료 초대(EXPIRED, 만료된 PENDING) 제외, ACCEPTED/DECLINED는 표시. status=PENDING → 유효한 것만. status=EXPIRED → EXPIRED와 만료된 PENDING 둘 다.
  • 이미 답한(ACCEPTED/DECLINED) 카드도 같은 409 증상이 나므로 MyInvitationResponseDtostatus({value, description})를 추가했습니다. 만료된 PENDING은 EXPIRED로 표시됩니다.

P0-2 — 초대 발송 경쟁 조건 (리뷰에서 추가)

  • SendWorkspaceInvitation이 조회 후 저장 구조이고 유니크 제약이 없어 동시 요청 시 같은 사용자에게 PENDING 초대가 중복 생성될 수 있었습니다.
  • V15 business_invitations (workspace_id, user_id) WHERE status = 'PENDING' 부분 유니크 인덱스. 마이그레이션이 먼저 만료된 PENDING과 중복 PENDING(최신 id 외)을 EXPIRED로 정리합니다.
  • 만료 행이 PENDING인 채로 남으면 재초대 insert가 이 인덱스에 걸리므로, 재초대 시 해당 사용자의 만료 PENDING을 expire()로 EXPIRED 처리하고 flush한 뒤 insert합니다(지연 만료, 스케줄러는 여전히 없음). 저장 시 인덱스 충돌이 나면 저장 대상을 ALREADY_INVITED로 응답합니다.

P1 — 퇴사한 근무자가 사장님 근무자 목록에 노출 (D-01)

  • GET /manager/workspaces/{id}/workersstatus 미입력 시 상태 조건을 걸지 않아 RESIGNED 근무자가 근무 배정 화면에 그대로 보였습니다.
  • status 미입력 시 ACTIVATED를 기본 적용하되, resignedAtFrom/To가 있으면 기본값을 적용하지 않습니다(퇴사자 조회 요청이 빈 결과가 되지 않도록). count·list가 같은 조건 메서드를 씁니다.

P2 — 초대 실패 응답이 원인을 구분하지 않음 (C-01)

  • 미가입 · 이미 근무 중 · 이미 초대 대기 중을 한 목록으로 모아 "발송할 수 없는 전화번호가 포함되어 있습니다." 하나로 응답해 클라이언트가 안내 문구를 만들 수 없었습니다.
  • 예외는 도메인 값 InvitationUnavailableDetail(phoneNumber, reason)만 담고, GlobalExceptionHandler가 adapter DTO InvitationUnavailableResponseDto로 바꿔 data로 내립니다. reason{value, description}이며 NOT_REGISTERED / ACCOUNT_UNAVAILABLE / ALREADY_WORKING / ALREADY_INVITED(판정 우선순위 그 순서). 정지 계정을 미가입으로 안내하던 문제는 findByContactIn의 ACTIVE 필터를 걷어내고 ACCOUNT_UNAVAILABLE로 구분합니다. ErrorCode는 B001(ILLEGAL_ARGUMENT) 그대로이고 Swagger 400 응답에 예시를 넣었습니다.

프론트 영향 (Breaking 주의)

  • POST /manager/workspaces/{id}/invitations 실패 응답 data: ["01012345678", ...][{"phoneNumber": "01012345678", "reason": {"value": "NOT_REGISTERED", "description": "가입되지 않은 번호입니다."}}, ...]. 에러 코드 B001·메시지는 동일합니다.
  • GET /app/users/me/invitations: 만료된 초대가 기본 목록에서 빠지고(status=EXPIRED로 조회 가능), 응답에 status: {value, description}가 추가됩니다. PENDING이 아닌 카드는 수락·거절 버튼을 숨겨 주세요.
  • GET /manager/workspaces/{id}/workers: status 미입력 시 재직자만 반환. 퇴사자는 status=RESIGNED 또는 resignedAtFrom/To로 조회합니다.

테스트

  • BusinessInvitationQueryRepositoryImplTests(@DataJpaTest) — 만료 PENDING 제외, 목록 규칙(필터 없음 / PENDING / EXPIRED), 만료 PENDING 조회
  • WorkspaceQueryRepositoryImplWorkerListTests(@DataJpaTest) — status 미지정 시 RESIGNED 제외, RESIGNED 지정, 퇴사일 필터만 지정
  • BusinessInvitationTestsgetEffectiveStatus·만료 경계(expiresAt == now)
  • SendWorkspaceInvitationTests — 사유 4종·혼합·우선순위·정지 계정·만료 PENDING 지연 처리 순서·유니크 충돌 → ALREADY_INVITED
  • 전체 ./gradlew test 433건 통과 (skip 2건은 기존 STOMP Redis 통합 테스트)

남긴 것

  • 초대 만료 스케줄러 미도입 — EXPIRED는 재초대 시점에만 지연 처리되므로 재초대가 없는 만료 PENDING 행은 누적됩니다(목록에선 제외). 이력 조회 요구가 생기면 WorkspaceScheduleServiceImpl 패턴으로 추가 예정.
  • V15 부분 유니크 인덱스는 테스트 DB(H2, ddl-auto=create, Flyway off)에서 검증되지 않습니다 — V10/V14와 동일한 기존 공백이며 백로그의 Flyway smoke test 항목이 덮습니다.
  • GetMyInvitationListpageSize + 1/hasNext 처리를 하지 않아 마지막 페이지에도 커서를 내리는 선재 결함이 있습니다(이 PR 범위 밖, 백로그 등록).
  • User.java@SQLRestriction이 필드에 붙어 있어 사실상 no-op으로 보입니다(선재, 백로그 등록). 탈퇴 사용자는 withdraw()의 contact 익명화로 걸러집니다.

QA 통합 테스트(C-04, D-01, C-01)에서 확인된 결함 3건.

- 만료된 PENDING 초대가 목록에 남아 수락·거절이 모두 409 나고 같은 번호 재초대가 막히던 문제.
  만료 스케줄러가 없어 DB status가 PENDING으로 남으므로, 목록·건수 조회와
  "이미 초대 대기 중" 판정을 expiresAt 기준 쿼리 조건으로 처리한다.
  이미 답한(ACCEPTED/DECLINED) 카드도 같은 증상이라 응답에 status를 추가한다.
- 사장님 근무자 목록이 status 미입력 시 퇴사자까지 노출되던 문제. 기본값을 ACTIVATED로 고정한다.
  퇴사자 조회는 status=RESIGNED를 명시해야 한다.
- 초대 실패 응답이 미가입·재직 중·초대 대기 중을 구분하지 않던 문제.
  data를 [{phoneNumber, reason}] 형태로 바꾼다(Breaking, ErrorCode는 B001 유지).

@DataJpaTest 리포지토리 테스트 2개 신설, SendWorkspaceInvitation 테스트에 사유·우선순위 케이스 추가.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Repository: alter-app/alter-backend/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 6efd411f-3f04-4c52-a33d-c0ae42ac705d


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.

초대 발송이 조회 후 저장 구조라 동시 요청 시 PENDING 초대가 중복 생성되던 경쟁 상황을
V15 부분 유니크 인덱스(status = PENDING)로 DB 차원에서 막는다. 만료된 PENDING 은 인덱스에
걸리므로 재초대 전에 EXPIRED 로 바꿔 flush 하고, 충돌 시 ALREADY_INVITED 사유로 응답한다.

private BooleanExpression eqWorkerStatus(QWorkspaceWorker qWorkspaceWorker, WorkspaceWorkerStatus status) {
return status != null ? qWorkspaceWorker.status.eq(status) : null;
return qWorkspaceWorker.status.eq(status != null ? status : WorkspaceWorkerStatus.ACTIVATED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[결함] status를 비우면 ACTIVATED가 강제로 걸려 기존 조회가 깨집니다

status가 null이면 무조건 ACTIVATED를 걸어서, 아래 요청이 오류 없이 빈 결과를 냅니다.

  • 근무자 목록에 resignedAtFrom만 넣고 status를 비움 → status = ACTIVATED AND resigned_at >= … → 항상 0건
  • 재직자와 퇴사자를 한 번에 보던 기존 호출도 이제 재직자만 받습니다.

퇴사일 필터(resignedAtFrom/resignedAtTo)가 있으면 기본값을 적용하지 않는 방법을 제안합니다.

이 줄을 그대로 두더라도 삼항 연산자 대신 ObjectUtils.defaultIfNull(status, WorkspaceWorkerStatus.ACTIVATED)를 써 주세요. null 처리는 org.apache.commons.lang3 유틸을 쓰는 게 컨벤션이고, 같은 파일에서도 이미 ObjectUtils를 쓰고 있습니다.

|| pendingInvitedUserIds.contains(invitedUser.getId())) {
unavailablePhoneNumbers.add(phoneNumber);
if (invitedUser == null) {
details.add(new InvitationUnavailableDetail(phoneNumber, InvitationUnavailableReason.NOT_REGISTERED));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[결함] 정지(SUSPENDED)된 가입자가 '미가입'으로 안내됩니다

userQueryRepository.findByContactInstatus = ACTIVE인 사용자만 돌려줍니다(UserQueryRepositoryImpl.java:137). 그래서 가입했지만 정지된 번호도 contactToUser에서 빠지고, 여기서 NOT_REGISTERED가 됩니다.

조회 조건은 이 PR 전부터 있던 것이지만, 실패 사유를 구분하자는 C-01의 목적과 어긋나서 같이 남깁니다. 정지 계정용 사유를 따로 두거나, 최소한 API 설명에 "미가입에는 이용할 수 없는 계정도 포함된다"고 적어 두면 좋겠습니다.


private BooleanExpression notExpiredPendingCondition(QBusinessInvitation q) {
return q.status.ne(BusinessInvitationStatus.PENDING)
.or(q.expiresAt.gt(LocalDateTime.now()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[결함] 만료 기준 시각이 쿼리마다 다르고, 엔티티와도 다릅니다

  • countByUserfindByUserWithCursor가 각자 LocalDateTime.now()를 부릅니다. 두 호출 사이에 초대 하나가 만료되면 totalCount는 3인데 목록은 2건처럼 어긋날 수 있습니다.
  • 경계값도 다릅니다. 쿼리는 expiresAt > now만 유효로 보는데, 엔티티 isExpired()now.isAfter(expiresAt)라서 expiresAt == now이면 유효로 봅니다. 목록에서는 숨긴 초대를 수락은 허용하는 셈입니다.

UseCase에서 now를 한 번만 구해 넘기고, 경계 기준(gt/goe)을 엔티티와 하나로 맞추는 방법을 제안합니다.

qBusinessInvitation.status.eq(BusinessInvitationStatus.PENDING),
qBusinessInvitation.invitedUser.id.in(userIds)
qBusinessInvitation.invitedUser.id.in(userIds),
qBusinessInvitation.expiresAt.gt(LocalDateTime.now())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[설계] 'PENDING이면서 만료 전' 판정이 네 곳에 흩어져 있습니다

  • 이 줄: expiresAt.gt(now)
  • 64줄 findExpiredPendingByWorkspaceAndUserIds: expiresAt.loe(now)
  • 129~131줄 notExpiredPendingCondition
  • BusinessInvitation.isExpired()

만료된 초대가 DB에는 PENDING으로 남아 있어서, 앞으로 PENDING을 읽는 쿼리를 추가하다가 만료 조건을 하나라도 빠뜨리면 C-04가 다시 생깁니다. "PENDING이면서 만료 전"과 "PENDING이면서 만료됨" 조건을 헬퍼 한 쌍으로 모아 세 쿼리가 함께 쓰면 좋겠습니다.

public class MyInvitationListFilterDto {

@Parameter(description = "상태 필터 (PENDING | ACCEPTED | DECLINED | EXPIRED), 미입력 시 전체 조회")
@Parameter(description = "상태 필터 (PENDING | ACCEPTED | DECLINED | EXPIRED — EXPIRED는 현재 미사용), 미입력 시 전체 조회, 만료된 PENDING 초대는 항상 제외")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[결함] d222741 이후 'EXPIRED는 현재 미사용' 설명이 틀리고, 만료된 초대가 행마다 다르게 보입니다

d2227416부터 EXPIRED가 실제로 저장됩니다.

  • V15 마이그레이션이 배포 시점에 이미 만료된 PENDING을 모두 EXPIRED로 바꿉니다.
  • 재초대할 때도 이전 초대를 EXPIRED로 바꿉니다.

그런데 목록 조회는 여전히 "만료된 PENDING만 숨김" 기준이라 이렇게 됩니다.

  • 필터 없이 조회하면 EXPIRED로 바뀐 초대는 status: EXPIRED보이고, 배포 뒤에 만료된 PENDING 초대는 숨겨집니다. 똑같이 만료된 초대인데 마이그레이션이나 재초대를 거쳤는지에 따라 보이거나 숨겨집니다.
  • status=EXPIRED 필터는 그중 일부만 돌려줍니다.

만료된 초대를 목록에 보여줄지 숨길지 먼저 정하고, EXPIRED 초대와 만료된 PENDING 초대를 같은 규칙으로 다뤄 주세요. 예를 들어 status=EXPIRED를 "EXPIRED이거나 만료된 PENDING"으로 해석하는 방법이 있습니다. UserWorkspaceInvitationControllerSpec.java:46의 같은 문구도 함께 고쳐야 합니다.

assertThat(result).extracting(ManagerWorkspaceWorkerListResponse::getId)
.containsExactly(resigned.getId());
assertThat(result).extracting(r -> r.getStatus())
.containsExactly(WorkspaceWorkerStatus.RESIGNED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[테스트] RESIGNED 케이스에 건수 검증이 없습니다

바로 위 '미지정' 케이스는 getWorkspaceWorkerCount까지 확인하는데, 이 케이스는 목록만 봅니다. count 쿼리의 status 조건이 틀려도 이 테스트는 통과합니다. assertThat(count).isEqualTo(1)을 추가해 주세요.

WorkspaceQueryRepositoryImpl 677줄 코멘트의 상황(퇴사일 필터만 넣고 status를 비운 요청)을 확인하는 케이스도 있으면 좋겠습니다.


private User saveUser() {
User user = User.create(
"010" + String.valueOf(System.nanoTime()).substring(0, 8), "encoded", "김알바",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[테스트] nanoTime 앞 8자리를 잘라 써서 번호가 겹칠 수 있습니다

String.valueOf(System.nanoTime()).substring(0, 8)은 값의 앞자리를 씁니다. nanoTime은 보통 시스템 가동 시간 기준이라 자릿수가 크면 앞 8자리가 몇 밀리초 넘게 그대로입니다. 그러면 한 테스트 안에서 만든 사용자들이 같은 번호를 받습니다.

지금은 contact에 유니크 제약이 없어서 통과합니다. 하지만 이 헬퍼로 findByContactIn이나 SendWorkspaceInvitation 통합 테스트를 만들면 Collectors.toMap이 중복 키로 IllegalStateException을 던집니다. 뒷자리를 쓰거나 AtomicInteger 카운터로 바꿔 주세요. WorkspaceQueryRepositoryImplWorkerListTests.java:61도 같습니다.

private String businessName;

@Schema(description = "초대 상태", example = "PENDING")
private BusinessInvitationStatus status;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[컨벤션] 응답 enum은 DescribedEnumDto로 내려 주세요

컨벤션상 응답에 나가는 enum은 static Map<EnumType, String> describe()를 구현하고, DTO에서는 DescribedEnumDto.of(enumValue, EnumClass.describe())로 씁니다(예: AdminWorkspaceRequestResponseDto). 지금은 BusinessInvitationStatus를 그대로 내보내고, 이 enum에는 describe()도 없습니다. 이대로면 클라이언트가 상태 표시 문구를 따로 만들어야 합니다.

@@ -0,0 +1,5 @@
package com.dreamteam.alter.domain.workspace.type;

public enum InvitationUnavailableReason {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[컨벤션] 실패 사유 enum에도 describe()가 필요합니다

초대 실패 응답(data[].reason)으로 나가는 enum이라 위와 같은 규칙이 적용됩니다. 사유별 안내 문구를 보여주는 것이 C-01의 목적이니, describe()로 문구를 함께 내려 주면 클라이언트가 문구를 따로 관리하지 않아도 됩니다.

import com.dreamteam.alter.domain.workspace.type.InvitationUnavailableReason;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "초대 발송 불가 번호와 사유")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[컨벤션] 응답용 record가 domain 패키지에 있고, 붙인 @Schema는 문서에 나오지 않습니다

  • 컨벤션상 @Schema가 붙은 응답 DTO는 adapter/inbound/.../dto/에 둡니다. 지금 domain 패키지에서 io.swagger를 import하는 클래스는 이 파일 하나뿐입니다.
  • 게다가 ManagerWorkspaceInvitationControllerSpec의 400 응답에 content/schema 지정이 없어서 springdoc이 이 record를 문서에 넣지 않습니다. ca91af34에서 붙인 @Schema는 실제로 효과가 없습니다.

예외에는 domain 값(번호, 사유)만 담고, 응답 변환과 @Schema는 adapter DTO에서 맡는 방법을 제안합니다. 문서에 보이게 하려면 400 응답에 content = @Content(schema = @Schema(implementation = ...))를 지정해 주세요.

…TO 정리

리뷰(ysw789) 10건을 반영한다.

- 근무자 목록 status 기본값: resignedAtFrom/To가 있으면 ACTIVATED 기본을 적용하지 않는다.
  퇴사일 필터만 넣은 요청이 빈 결과가 되던 문제. null 처리는 ObjectUtils.defaultIfNull.
- 만료 기준 시각을 UseCase가 한 번 구해 리포지토리 4메서드와 응답 DTO에 넘긴다.
  count와 list가 각자 now()를 불러 어긋날 수 있었고, 엔티티 isExpired() 경계도 쿼리와 달랐다.
  "PENDING이면서 유효/만료" 조건을 pendingAndValid/pendingAndExpired 헬퍼 한 쌍으로 모은다.
- V15 이후 EXPIRED 행과 만료된 PENDING 행이 공존하므로 목록 규칙을 통일한다.
  필터 없음 → 둘 다 제외, status=EXPIRED → 둘 다, status=PENDING → 유효한 것만.
  응답 status는 getEffectiveStatus(now)로 만료된 PENDING을 EXPIRED로 표시한다.
- 정지 계정이 미가입으로 안내되던 문제: findByContactIn의 ACTIVE 필터를 제거하고
  ACCOUNT_UNAVAILABLE 사유를 추가한다(호출처는 SendWorkspaceInvitation 하나).
- 응답 enum은 describe() + DescribedEnumDto로 내린다. 실패 상세 응답 DTO를 adapter로 옮기고
  도메인 record에서 @Schema를 걷어낸다. Swagger 400 응답에 @content 예시를 지정한다.
- 테스트: AtomicInteger 픽스처, RESIGNED count 단언, 퇴사일 필터 케이스, PENDING 필터 케이스,
  엔티티 만료 경계 테스트 신설.
@@ -57,36 +63,71 @@ public void execute(ManagerActor actor, Long workspaceId, SendWorkspaceInvitatio
.stream().collect(Collectors.toMap(User::getContact, Function.identity()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[결함] 같은 번호의 계정이 둘이면 여기서 IllegalStateException이 나고 500으로 끝납니다

1c966322에서 findByContactIn의 ACTIVE 조건을 빼면서 한 번호에 계정이 둘 이상 조회될 수 있게 됐습니다. 그러면 이 Collectors.toMap이 중복 키로 IllegalStateException을 던집니다.

실제로 생길 수 있는 경로입니다.

  1. 사용자 A가 정지(SUSPENDED)됩니다(AdminUpdateUserStatus).
  2. 같은 번호로 새로 가입합니다. 가입 중복 검사가 쓰는 findByContact는 ACTIVE 계정만 보므로 통과하고, 사용자 B(ACTIVE)가 생깁니다. users.contact에는 유니크 제약도 없습니다.
  3. 사장님이 이 번호로 초대하면 A와 B가 함께 조회돼 여기서 예외가 나고 500이 됩니다.

B는 정상 계정이라 초대가 돼야 하는데, 번호별 실패 응답도 아닌 서버 오류가 납니다. 이전에는 ACTIVE만 조회해서 없던 문제입니다.

번호별로 ACTIVE 계정을 먼저 고르도록 toMap에 중복 처리 함수를 넣어 주세요. 예:

.collect(Collectors.toMap(User::getContact, Function.identity(),
    (a, b) -> a.getStatus() == UserStatus.ACTIVE ? a : b));

지금 정지 계정 테스트는 조회 결과를 계정 하나로만 가정하니, 같은 번호에 SUSPENDED·ACTIVE 계정이 함께 오는 케이스도 추가해 주세요.

try {
businessInvitationRepository.saveAll(invitationsToSave);
} catch (DataIntegrityViolationException e) {
// ponytail: 부분 유니크 인덱스 충돌은 같은 요청의 동시 제출이 대부분이라 저장 대상 전부를 ALREADY_INVITED 로 본다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[사소] 주석 앞의 ponytail:은 실수로 들어간 단어로 보입니다. 지워 주세요.

}

@Test
void isExpired_expiresAt이_now와_같으면_만료() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[사소] 테스트 이름과 검증 대상이 다릅니다

이름은 isExpired_...인데 실제로는 getEffectiveStatus(now)를 검증합니다. 두 메서드가 같은 내부 판정(isExpiredAt)을 써서 동작은 맞습니다. isExpired()는 안에서 now()를 직접 불러 경계값을 테스트하기 어려우니, 이름을 getEffectiveStatus_expiresAt이_now와_같으면_EXPIRED처럼 검증 대상에 맞춰 주세요.

This branch has not been deployed

No deployments
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.

2 participants