Skip to content

docs: 투표 기능 API 스펙 및 구현 계획 문서 추가#90

Open
Junhyukkkk wants to merge 1 commit intodevelopfrom
feat/add-vote-api-imple-docs
Open

docs: 투표 기능 API 스펙 및 구현 계획 문서 추가#90
Junhyukkkk wants to merge 1 commit intodevelopfrom
feat/add-vote-api-imple-docs

Conversation

@Junhyukkkk
Copy link
Copy Markdown
Member

Summary

  • docs/spec/2026-05-04-vote-api-spec.md: 투표 도메인 설계, 일반형/몰입형/결과 REST API 스펙, 비회원 쿠키 식별 정책, WebSocket 실시간 비율 구조 정의
  • docs/plan/2026-05-04-vote-implementation-plan.md: 헥사고날 아키텍처 + DDD 기반 구현 계획 (패키지 구조, 단계별 구현, 채팅 도메인 mock 흡수 전략, 테스트 전략)

문서 구조

docs/
├── spec/   # 설계 문서 (무엇을 만들 것인가)
└── plan/   # 구현 문서 (어떻게 만들 것인가)

Test plan

  • 문서 내용 검토
  • API 스펙 프론트 팀과 싱크
  • 채팅 도메인 mock 흡수 범위 @tlarbals824 와 협의 (Vote 엔티티, VoteParticipation, Repository 인터페이스)

Copy link
Copy Markdown
Collaborator

@tlarbals824 tlarbals824 left a comment

Choose a reason for hiding this comment

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

코맨트 남겨뒀습니다! 수고하셨어요!

Comment on lines +565 to +568
@PostConstruct
public void closeExpiredOnStartup() {
closeExpiredVotes();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

너무 좋은 접근이야. 다만 서버 시작할 때, 해당 함수로 인해 http 요청이 block되는지 찾아보면 좋을 것 같아. 만약 그렇다면 비동기로 처리하도록해주면 좋을듯!

Comment on lines +118 to +119
private String thumbnailUrl;
private String imageUrl;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

어떤차이가 있는거에요?

private VoteDuration duration;
@Enumerated(EnumType.STRING)
private VoteStatus status;
private LocalDateTime endAt;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

인스턴스 또는 컨테이너의 설정에 따라서 시간들이 제각각일 수 있어 보여요. 혹시 Instant(UTC)로 저장하는건 어떻게 생각하시나요?

Comment on lines +120 to +121
@Enumerated(EnumType.STRING)
private VoteDuration duration;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

VoteDuration은 뷰 관심사와 가까워보여요! endAt을 외부에서 계산해서 받는다면, 해당 타입은 없어도 괜찮아보이는데 어떻게 생각하시나요?

Comment on lines +130 to +132
if (type == VoteType.IMMERSIVE && imageUrl == null) {
throw new ImageRequiredException();
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines +297 to +302
public interface VoteCommandUseCase {
VoteCreateResult create(VoteCreateCommand command);
ParticipateResult participateAsMember(Long voteId, Long userId, Long optionId);
ParticipateResult participateAsGuest(Long voteId, String anonymousId, Long optionId);
void cancel(Long voteId, Long userId);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

인터페이스 구성이 명확하네요!

@Override
public VoteCreateResult create(VoteCreateCommand cmd) {
// 1. duration 검증 (enum 팩토리에 위임)
VoteDuration duration = VoteDuration.from(cmd.durationHours());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

오히려 외부에서는 enum으로 받고, 내부 커맨드로 넘어올 때에는 시간을 받는게 자연스러워보여요!

왜냐하면 프론트에서 넘겨줄 수 있는 값에 제한이 필요하고 그 제한을 ENUM이 대신해줄 수 있을 것 같아서요! 그리고 내부 도메인은 외부에서 어떤 enum인지 상관 없이 특정 시간동안 유효하다고 값만 받으면 되니까요!

AS-IS

public record VoteCreateCommand(
    VoteType type,
    String title,
    String content,
    String thumbnailUrl,
    String imageUrl,
    int durationHours,
    String optionA,
    String optionB
) { }


VoteDuration duration = VoteDuration.from(cmd.durationHours());

TO-BE

public record VoteCreateCommand(
    VoteType type,
    String title,
    String content,
    String thumbnailUrl,
    String imageUrl,
    VoteDuration durations,
    String optionA,
    String optionB
) { }

int duration = command.durations.toDurationValue(); // 러프하게 작성했습니다.

}

AiInsight aiInsight = (userId != null && hasParticipated(voteId, userId))
? aiInsightService.generateOrFetch(voteId, userId)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ai 인사이트의 경우 언제 생성될까요? 사용자가 조회할때라면 ai 호출로 인해 크게 지연될 수 있어보여요! 만약에 사용자에게 생성중이에요!를 제공할 수 있다면 비동기로 풀어볼 수 있어보여요

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

그게 아니라면, 투표가 완료되면 스케쥴러를 통해 생성할수도 있구요!

- 첫 호출 시 LLM 호출 → vote 테이블의 `ai_insight_headline`, `ai_insight_body` 컬럼에 저장
- 이후 호출은 컬럼에서 직접 반환
- LLM 호출 실패 시 `aiInsight.available: false`로 응답 (에러 아님)
- 본인이 만든 youth-policy AI 에이전트 (LangChain4j + Gemini 2.5 Flash) 인프라 재활용 검토
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

// After
@RequiredArgsConstructor
public class ChatCommandService {
private final VoteQueryUseCase voteQueryUseCase; // ← inbound port 의존
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines +133 to +141
Vote vote = new Vote();
vote.type = type;
vote.title = title;
vote.content = content;
vote.thumbnailUrl = thumbnailUrl;
vote.imageUrl = imageUrl;
vote.duration = duration;
vote.status = VoteStatus.ONGOING;
vote.endAt = LocalDateTime.now().plusHours(duration.getHours());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

뭔가 빌더 패턴을 적용하면 더 깔끔할거 같아요!

Comment on lines +187 to +188
private Long userId; // 회원이면 set
private String anonymousId; // 비회원이면 set
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

비회원은 아직 생각은 안해봤는데 가능하다면 하나의 테이블로 관리하는게 더 편해보입니다 결국 회원과 비회원이 모두 같은 기능을 사용해서 분기 없이 참조하는게 더 편해보일것 같습니다

Comment on lines +595 to +622
@PostMapping
public VoteCreateResponse create(
@AuthenticationPrincipal Long userId,
@RequestBody @Valid VoteCreateRequest request) {
if (userId == null) throw new UnauthorizedException();
return VoteCreateResponse.from(
voteCommandUseCase.create(request.toCommand()));
}

@GetMapping("/{voteId}")
public VoteDetailResponse getDetail(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId) { ... }

@PostMapping("/{voteId}/participate")
public ParticipateResponse participate(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId,
@AnonymousId String anonymousId,
@RequestBody ParticipateRequest request) { ... }

@DeleteMapping("/{voteId}/participate")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void cancel(
@PathVariable Long voteId,
@AuthenticationPrincipal Long userId) { ... }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

해당 문법은 처음보네요 배워갑니다!!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants