Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ab79baf
[refactor] dummy class delete (#171)
seongjunnoh Aug 8, 2025
f0d656a
[feat] 오늘의 한마디 작성 use case 추가 (#171)
seongjunnoh Aug 8, 2025
64416f8
[feat] 오늘의 한마디 작성 use case 구현 (#171)
seongjunnoh Aug 8, 2025
77aa623
[feat] 출석체크 도메인 내부에 팩토리 메서드 추가 (#171)
seongjunnoh Aug 8, 2025
68401f1
[feat] 생성한 출석체크 도메인 저장 로직 구현 (#171)
seongjunnoh Aug 8, 2025
1319c76
[feat] 오늘의 한마디 api 웹 어댑터 계층 코드 구현 (#171)
seongjunnoh Aug 8, 2025
0ac3659
[docs] 오늘의 한마디 api 스웨거 description 작성 (#171)
seongjunnoh Aug 8, 2025
5b0c8ba
[test] 오늘의 한마디 controller 단위 테스트 코드 작성 (#171)
seongjunnoh Aug 8, 2025
d7e603b
[test] 오늘의 한마디 api 통합 테스트 코드 작성 (#171)
seongjunnoh Aug 8, 2025
4fdc327
Merge remote-tracking branch 'origin' into feat/#171-create-and-show-…
seongjunnoh Aug 8, 2025
e8fbc02
[docs] 오늘의 한마디 api 스웨거 error 명세 추가 (#171)
seongjunnoh Aug 9, 2025
a096083
Merge remote-tracking branch 'origin' into feat/#171-create-attendanc…
seongjunnoh Aug 12, 2025
36589ac
[move] attendanceCheck 패키지의 모든 파일을 room 패키지 하위로 이동 (#171)
seongjunnoh Aug 12, 2025
d02b355
[refactor] 바뀐 요구사항에 따라 오늘의 한마디 작성 api service 코드 수정 (#171)
seongjunnoh Aug 13, 2025
37e98af
[refactor] AttendanceCheck 도메인 생성 시 검증을 수행하도록 도메인 규칙 코드 추가 (#171)
seongjunnoh Aug 13, 2025
298e3cc
[refactor] 바뀐 요구사항에 따라 use case 수정 및 result dto 추가 (#171)
seongjunnoh Aug 13, 2025
cdeff6f
[refactor] 바뀐 요구사항에 따라 response dto 수정 (#171)
seongjunnoh Aug 13, 2025
243fd73
[feat] 유저가 오늘 하루동안 오늘의 한마디를 몇개 작성하였는지 계산하는 코드 추가 (#171)
seongjunnoh Aug 13, 2025
c86d2f9
[feat] 오늘의 한마디 작성 제한 조건이 추가됨에 따라, error code 추가 및 swagger 명세 추가 (#171)
seongjunnoh Aug 13, 2025
5aded9c
[refactor] 요구사항이 수정됨에 따라 오늘의 한마디 작성 api 통합 테스트 코드 수정 (#171)
seongjunnoh Aug 13, 2025
85b9303
[test] AttendanceCheck 도메인 단위 테스트 코드 작성 (#171)
seongjunnoh Aug 13, 2025
5c8ae44
Merge remote-tracking branch 'origin' into feat/#171-create-attendanc…
seongjunnoh Aug 13, 2025
87ac1af
[refactor] 패키지 네이밍 수정 attendanceCheck -> attendancecheck (#171)
seongjunnoh Aug 14, 2025
b21077d
[refactor] 유저가 특정 방에서 작성한 오늘의 한마디를 조회하도록 수정 (#171)
seongjunnoh Aug 14, 2025
155259b
[refactor] 유저가 특정 방에서 오늘 하루동안 작성한 ACTIVE 상태인 오늘의 한마디 개수를 조회할 수 있도록 jp…
seongjunnoh Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ public enum ErrorCode implements ResponseCode {
COMMENT_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, 190005, "댓글 삭제 권한이 없습니다."),
COMMENT_COUNT_UNDERFLOW(HttpStatus.BAD_REQUEST, 190007, "댓글 수는 0 이하로 감소할 수 없습니다."),

/**
* 195000 : AttendanceCheck error
*/
ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, 195000, "오늘의 한마디 작성 가능 횟수를 초과하였습니다."),

;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ public enum SwaggerResponseDescription {
RECENT_SEARCH_NOT_ADDED_BY_USER
))),

// Attendance Check
ATTENDANCE_CHECK_CREATE(new LinkedHashSet<>(Set.of(
ROOM_ACCESS_FORBIDDEN,
ROOM_NOT_FOUND,
USER_NOT_FOUND,
ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED
))),

;
private final Set<ErrorCode> errorCodeList;
SwaggerResponseDescription(Set<ErrorCode> errorCodeList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import konkuk.thip.room.adapter.in.web.request.AttendanceCheckCreateRequest;
import konkuk.thip.room.adapter.in.web.response.AttendanceCheckCreateResponse;
import konkuk.thip.room.application.port.in.AttendanceCheckCreateUseCase;
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import konkuk.thip.common.swagger.annotation.ExceptionDescription;
Expand Down Expand Up @@ -35,6 +38,7 @@ public class RoomCommandController {
private final RoomJoinUseCase roomJoinUsecase;
private final RoomRecruitCloseUseCase roomRecruitCloseUsecase;
private final PostLikeUseCase postLikeUseCase;
private final AttendanceCheckCreateUseCase attendanceCheckCreateUseCase;

/**
* 방 생성 요청
Expand Down Expand Up @@ -102,4 +106,22 @@ public BaseResponse<RoomPostIsLikeResponse> likeRoomPost(
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(RoomPostIsLikeResponse.of(postLikeUseCase.changeLikeStatusPost(request.toCommand(userId, postId))));
}

/**
* 오늘의 한마디 작성
*/
@Operation(
summary = "오늘의 한마디 작성",
description = "방 참여자가 오늘의 한마디를 작성합니다."
)
@ExceptionDescription(ATTENDANCE_CHECK_CREATE)
@PostMapping("/rooms/{roomId}/daily-greeting")
public BaseResponse<AttendanceCheckCreateResponse> createFeed(
@RequestBody @Valid final AttendanceCheckCreateRequest request,
@Parameter(description = "오늘의 한마디를 작성할 방 id값") @PathVariable("roomId") final Long roomId,
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(AttendanceCheckCreateResponse.of(
attendanceCheckCreateUseCase.create(request.toCommand(userId, roomId))
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package konkuk.thip.room.adapter.in.web.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateCommand;

public record AttendanceCheckCreateRequest(
@Schema(description = "유저가 작성한 오늘의 한마디 내용")
@NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
String content
) {
Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 길이 제약 및 예시 추가 제안

DB 컬럼 길이와 정합성을 맞추기 위해 최대 길이 제한을 권장합니다. 또한 Swagger 예시를 제공하면 클라이언트 UX가 좋아집니다.

 public record AttendanceCheckCreateRequest(
-        @Schema(description = "유저가 작성한 오늘의 한마디 내용")
-        @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+        @Schema(description = "유저가 작성한 오늘의 한마디 내용", example = "오늘도 화이팅!")
+        @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+        @jakarta.validation.constraints.Size(max = 255, message = "오늘의 한마디 내용은 255자를 초과할 수 없습니다.")
         String content
 ) {

추가로 import가 필요 없다면 FQN 유지로 충분하지만, 선호하시면 상단에 import jakarta.validation.constraints.Size;를 추가하고 어노테이션에 FQN을 제거해도 됩니다.

📝 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
@Schema(description = "유저가 작성한 오늘의 한마디 내용")
@NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
String content
) {
public record AttendanceCheckCreateRequest(
@Schema(description = "유저가 작성한 오늘의 한마디 내용", example = "오늘도 화이팅!")
@NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
@jakarta.validation.constraints.Size(
max = 255,
message = "오늘의 한마디 내용은 255자를 초과할 수 없습니다."
)
String content
) {
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/in/web/request/AttendanceCheckCreateRequest.java
around lines 8 to 11, add a max-length constraint and a Swagger example for the
content field to match DB column limits and improve client UX: annotate the
field with a @Size(max = 255) (or the actual DB column max) and update @Schema
to include an example like @Schema(description = "유저가 작성한 오늘의 한마디 내용", example =
"오늘도 화이팅!") — optionally add import jakarta.validation.constraints.Size; at the
top and remove the FQN if you prefer imports.

public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) {
return new AttendanceCheckCreateCommand(creatorId, roomId, content);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package konkuk.thip.room.adapter.in.web.response;

import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateResult;

public record AttendanceCheckCreateResponse(
Long attendanceCheckId,
Long roomId,
boolean isFirstWrite
) {
public static AttendanceCheckCreateResponse of(AttendanceCheckCreateResult result) {
boolean isFirstWrite = false;
if (result.todayWriteCountOfUser() == 1) isFirstWrite = true;

return new AttendanceCheckCreateResponse(
result.attendanceCheckId(),
result.roomId(),
isFirstWrite
);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package konkuk.thip.attendancecheck.adapter.out.jpa;
package konkuk.thip.room.adapter.out.jpa;

import jakarta.persistence.*;
import konkuk.thip.common.entity.BaseJpaEntity;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import lombok.*;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package konkuk.thip.attendancecheck.adapter.out.mapper;
package konkuk.thip.room.adapter.out.mapper;

import konkuk.thip.attendancecheck.adapter.out.jpa.AttendanceCheckJpaEntity;
import konkuk.thip.attendancecheck.domain.AttendanceCheck;
import konkuk.thip.room.adapter.out.jpa.AttendanceCheckJpaEntity;
import konkuk.thip.room.domain.AttendanceCheck;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package konkuk.thip.room.adapter.out.persistence;

import konkuk.thip.room.adapter.out.mapper.AttendanceCheckMapper;
import konkuk.thip.room.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository;
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻 👍🏻

import konkuk.thip.room.application.port.out.AttendanceCheckCommandPort;
import konkuk.thip.room.domain.AttendanceCheck;
import konkuk.thip.common.exception.EntityNotFoundException;
import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity;
import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository;
import konkuk.thip.user.adapter.out.jpa.UserJpaEntity;
import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND;
import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND;

@Repository
@RequiredArgsConstructor
public class AttendanceCheckCommandPersistenceAdapter implements AttendanceCheckCommandPort {

private final AttendanceCheckJpaRepository attendanceCheckJpaRepository;
private final RoomJpaRepository roomJpaRepository;
private final UserJpaRepository userJpaRepository;
private final AttendanceCheckMapper attendanceCheckMapper;

@Override
public Long save(AttendanceCheck attendanceCheck) {
RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(attendanceCheck.getRoomId()).orElseThrow(
() -> new EntityNotFoundException(ROOM_NOT_FOUND)
);

UserJpaEntity userJpaEntity = userJpaRepository.findById(attendanceCheck.getCreatorId()).orElseThrow(
() -> new EntityNotFoundException(USER_NOT_FOUND)
);

return attendanceCheckJpaRepository.save(
attendanceCheckMapper.toJpaEntity(attendanceCheck, roomJpaEntity, userJpaEntity)
).getAttendanceCheckId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package konkuk.thip.room.adapter.out.persistence;

import konkuk.thip.room.adapter.out.mapper.AttendanceCheckMapper;
import konkuk.thip.room.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository;
import konkuk.thip.room.application.port.out.AttendanceCheckQueryPort;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;

import static konkuk.thip.common.entity.StatusType.ACTIVE;

@Repository
@RequiredArgsConstructor
public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort {

private final AttendanceCheckJpaRepository attendanceCheckJpaRepository;
private final AttendanceCheckMapper attendanceCheckMapper;

@Override
public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) {
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
LocalDateTime endOfDay = startOfDay.plusDays(1);

return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetween(userId, roomId, startOfDay, endOfDay, ACTIVE);
Comment on lines +22 to +25
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

시간대 의존성 및 테스트 용이성 개선: Clock 주입으로 교체

LocalDateTime.now()는 서버/OS/JVM 타임존에 의존합니다. 운영/개발 환경 타임존을 맞추셨더라도, 테스트 용이성과 이식성을 위해 Clock을 주입받아 사용하시길 권장합니다(비즈니스 타임존이 Asia/Seoul이라면 해당 Zone으로 고정된 Clock 빈 권장).

아래와 같이 적용 가능합니다(별도의 @Configuration에서 Clock.system(ZoneId.of("Asia/Seoul")) 빈 제공 가정).

+import java.time.Clock;
@@
-public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort {
+public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort {
@@
-    public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) {
-        LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
-        LocalDateTime endOfDay = startOfDay.plusDays(1);
+    private final Clock clock;
+
+    public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) {
+        LocalDateTime startOfDay = LocalDateTime.now(clock).toLocalDate().atStartOfDay();
+        LocalDateTime endOfDay = startOfDay.plusDays(1);

추가(별도 파일) 예시:

// 예: TimeConfig.java
@Bean
public Clock clock() {
    return Clock.system(ZoneId.of("Asia/Seoul"));
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
around lines 22 to 25, replace the direct use of LocalDateTime.now() with an
injected java.time.Clock to remove timezone/OS dependency: add a Clock field
injected via constructor (or @Autowired), then compute startOfDay as
LocalDate.now(clock).atStartOfDay() and endOfDay as startOfDay.plusDays(1);
ensure callers/DI provide a Clock bean (e.g.,
Clock.system(ZoneId.of("Asia/Seoul"))) and use a fixed Clock in tests for
determinism.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package konkuk.thip.room.adapter.out.persistence.repository.attendancecheck;

import konkuk.thip.common.entity.StatusType;
import konkuk.thip.room.adapter.out.jpa.AttendanceCheckJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;

public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

COUNT 반환 타입은 long 사용 권장

JPA COUNT는 Long으로 계산됩니다. 불필요한 narrowing을 피하려면 반환 타입을 long으로 두는 것이 안전합니다.

아래처럼 시그니처를 변경해주세요(어댑터/포트/서비스 호출부 전파 수정 필요).

-public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> {
+public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> {
@@
-    int countByUserIdAndRoomIdAndCreatedAtBetween(...);
+    long countByUserIdAndRoomIdAndCreatedAtBetween(...);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java
around line 11, the repository currently exposes a COUNT return type that is not
long; change the method signature(s) that return COUNT to use primitive long
instead of int/Long wrapper to match JPA COUNT semantics. Update the repository
interface signature to return long and then propagate that change through the
adapter, port, and service layers (update return types and any
callers/assignments) so all usages accept/handle a long. Ensure compilation by
adjusting method declarations, any interfaces, DTOs or mappers, and tests that
expect the previous type.


// TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯
@Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " +
"WHERE a.userJpaEntity.userId = :userId " +
"AND a.roomJpaEntity.roomId = :roomId " +
"AND a.status = :status " +
"AND a.createdAt >= :startOfDay " +
"AND a.createdAt < :endOfDay")
int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay, @Param("status")StatusType status);
}
Comment on lines +1 to +21
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

리포지토리 단위 테스트/슬라이스 테스트 보강 제안

경계시간(자정 직전/직후), 타임존(Asia/Seoul) 및 status 필터링(활성/삭제/만료)을 검증하는 슬라이스 테스트를 추가하면 회귀를 방지할 수 있습니다.

아래 스크립트로 관련 테스트 존재 여부를 확인해 주세요. 없다면 추가를 권장합니다.


🏁 Script executed:

#!/bin/bash
# 리포지토리/어댑터 테스트 존재 여부 점검
rg -n "AttendanceCheckJpaRepository|AttendanceCheckQueryPersistenceAdapter" src/test src/it || true

# 경계 케이스 키워드 탐색
rg -n "startOfDay|endOfDay|Asia/Seoul|ZoneId|Clock" src/test || true

Length of output: 569


테스트 보강 필요 — AttendanceCheckJpaRepository의 경계시간/타임존/상태 필터링 검증 추가

간단 검증 결과: src/test에서 AttendanceCheckCreateApiTest가 AttendanceCheckJpaRepository를 사용함을 확인했으나, "startOfDay|endOfDay|Asia/Seoul|ZoneId|Clock" 관련 키워드는 검색되지 않아 경계/타임존/상태 케이스에 대한 테스트가 없습니다.

확인된 위치

  • 검토 대상 레포지토리: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java
  • 현재 테스트: src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java (레포지토리 주입 확인)

권장 조치 (간단)

  • 리포지토리 슬라이스(@DataJpaTest 등) 또는 해당 어댑터용 단위/슬라이스 테스트 추가.
  • 필수 케이스: (1) 자정 직전/직후 경계 테스트, (2) 타임존(Asia/Seoul) 영향 검증 — 가능하면 Clock 주입으로 시간 제어, (3) status별 필터(활성/삭제/만료) 검증.
  • 테스트 위치 제안: src/test/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepositoryTest.java 또는 기존 AttendanceCheckCreateApiTest 확장.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package konkuk.thip.room.application.port.in;

import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateCommand;
import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateResult;

public interface AttendanceCheckCreateUseCase {

AttendanceCheckCreateResult create(AttendanceCheckCreateCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.room.application.port.in.dto;

public record AttendanceCheckCreateCommand(
Long creatorId,
Long roomId,
String content
) { }
Loading