From 829d0ce669ba75797d31ae32bb9a3b2100895794 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 18:57:27 +0900 Subject: [PATCH 01/25] =?UTF-8?q?[feat]=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api=20use=20case=20=EA=B0=9C=EB=B0=9C=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/DummyUseCase.java | 5 -- .../port/in/RoomCreateUseCase.java | 8 ++ .../application/port/in/dto/DummyCommand.java | 10 --- .../port/in/dto/RoomCreateCommand.java | 24 ++++++ .../service/RoomCreateService.java | 74 +++++++++++++++++++ .../room/application/service/RoomService.java | 11 --- 6 files changed, 106 insertions(+), 26 deletions(-) delete mode 100644 src/main/java/konkuk/thip/room/application/port/in/DummyUseCase.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java delete mode 100644 src/main/java/konkuk/thip/room/application/port/in/dto/DummyCommand.java create mode 100644 src/main/java/konkuk/thip/room/application/port/in/dto/RoomCreateCommand.java create mode 100644 src/main/java/konkuk/thip/room/application/service/RoomCreateService.java delete mode 100644 src/main/java/konkuk/thip/room/application/service/RoomService.java diff --git a/src/main/java/konkuk/thip/room/application/port/in/DummyUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/DummyUseCase.java deleted file mode 100644 index 62b144647..000000000 --- a/src/main/java/konkuk/thip/room/application/port/in/DummyUseCase.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.room.application.port.in; - -public interface DummyUseCase { - -} diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java new file mode 100644 index 000000000..0adb9306e --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.room.application.port.in; + +import konkuk.thip.room.application.port.in.dto.RoomCreateCommand; + +public interface RoomCreateUseCase { + + Long createRoom(RoomCreateCommand command); +} diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/DummyCommand.java b/src/main/java/konkuk/thip/room/application/port/in/dto/DummyCommand.java deleted file mode 100644 index b8ad36e4e..000000000 --- a/src/main/java/konkuk/thip/room/application/port/in/dto/DummyCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.room.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyCommand { - -} diff --git a/src/main/java/konkuk/thip/room/application/port/in/dto/RoomCreateCommand.java b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomCreateCommand.java new file mode 100644 index 000000000..874071f9c --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/in/dto/RoomCreateCommand.java @@ -0,0 +1,24 @@ +package konkuk.thip.room.application.port.in.dto; + +import java.time.LocalDate; + +public record RoomCreateCommand( + String isbn, + + String category, + + String roomName, + + String description, + + LocalDate progressStartDate, + + LocalDate progressEndDate, + + int recruitCount, + + String password, + + Boolean isPublic +) { +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java new file mode 100644 index 000000000..719081eac --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java @@ -0,0 +1,74 @@ +package konkuk.thip.room.application.service; + +import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.room.application.port.in.RoomCreateUseCase; +import konkuk.thip.room.application.port.in.dto.RoomCreateCommand; +import konkuk.thip.room.application.port.out.CategoryCommandPort; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Category; +import konkuk.thip.room.domain.Room; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RoomCreateService implements RoomCreateUseCase { + + private final RoomCommandPort roomCommandPort; + private final CategoryCommandPort categoryCommandPort; + private final BookCommandPort bookCommandPort; + private final BookApiQueryPort bookApiQueryPort; + + @Override + @Transactional + public Long createRoom(RoomCreateCommand command) { + // 1. Category 찾기 + Category category = categoryCommandPort.findByValue(command.category()); + + // 2. Book 찾기, 없으면 Book 로드 및 저장 + Long bookId = resolveBookAndEnsurePage(command.isbn()); + + // 3. Room 생성 및 저장 + Room room = Room.withoutId( + command.roomName(), + command.description(), + command.isPublic(), + command.password(), + 0, + command.progressStartDate(), + command.progressEndDate(), + command.recruitCount(), + bookId, + category.getId() + ); + + return roomCommandPort.save(room); + } + + private Long resolveBookAndEnsurePage(String isbn) { + try { + Book existing = bookCommandPort.findByIsbn(isbn); + if (!existing.hasPageCount()) { + updateBookPageCount(existing); + } + return existing.getId(); + } catch (EntityNotFoundException e) { + return saveNewBookWithPageCount(isbn); + } + } + + private void updateBookPageCount(Book book) { + Integer pageCount = bookApiQueryPort.findPageCountByIsbn(book.getIsbn()); + Book updated = book.changePageCount(pageCount); + bookCommandPort.updateForPageCount(updated); + } + + private Long saveNewBookWithPageCount(String isbn) { + Book loaded = bookApiQueryPort.loadBookWithPageByIsbn(isbn); + return bookCommandPort.save(loaded); + } +} diff --git a/src/main/java/konkuk/thip/room/application/service/RoomService.java b/src/main/java/konkuk/thip/room/application/service/RoomService.java deleted file mode 100644 index d0621a5c5..000000000 --- a/src/main/java/konkuk/thip/room/application/service/RoomService.java +++ /dev/null @@ -1,11 +0,0 @@ -package konkuk.thip.room.application.service; - -import konkuk.thip.room.application.port.in.DummyUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RoomService implements DummyUseCase { - -} From fddfc67c456fda412c6b7ab252109672ea9b448a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 18:59:16 +0900 Subject: [PATCH 02/25] =?UTF-8?q?[feat]=20Book,=20Room=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EC=97=90=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/book/domain/Book.java | 31 +++++++++++----- .../java/konkuk/thip/room/domain/Room.java | 36 ++++++++++++++++++- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/book/domain/Book.java b/src/main/java/konkuk/thip/book/domain/Book.java index 3b0e53723..b80382540 100644 --- a/src/main/java/konkuk/thip/book/domain/Book.java +++ b/src/main/java/konkuk/thip/book/domain/Book.java @@ -56,18 +56,31 @@ public static Book withoutId(String title, String isbn, String authorName, boole } public Book withId(Long id) { - return Book.builder() + return this.toBuilder() .id(id) - .title(this.title) - .isbn(this.isbn) - .authorName(this.authorName) - .bestSeller(this.bestSeller) - .publisher(this.publisher) - .imageUrl(this.imageUrl) - .pageCount(this.pageCount) - .description(this.description) .build(); } + public boolean hasPageCount() { + return pageCount != null && pageCount > 0; + } + + public Book changePageCount(Integer newPageCount) { + return this.toBuilder() + .pageCount(newPageCount) + .build(); + } + private BookBuilder toBuilder() { + return Book.builder() + .id(id) + .title(title) + .isbn(isbn) + .authorName(authorName) + .bestSeller(bestSeller) + .publisher(publisher) + .imageUrl(imageUrl) + .pageCount(pageCount) + .description(description); + } } diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index f3cf40a04..17afe4ae6 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -1,11 +1,14 @@ package konkuk.thip.room.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.exception.InvalidStateException; import lombok.Getter; import lombok.experimental.SuperBuilder; import java.time.LocalDate; +import static konkuk.thip.common.exception.code.ErrorCode.INVALID_ROOM_CREATE; + @Getter @SuperBuilder public class Room extends BaseDomainEntity { @@ -18,7 +21,7 @@ public class Room extends BaseDomainEntity { private boolean isPublic; - private Integer password; + private String password; private double roomPercentage; @@ -31,4 +34,35 @@ public class Room extends BaseDomainEntity { private Long bookId; private Long categoryId; + + public static Room withoutId(String title, String description, boolean isPublic, String password, double roomPercentage, LocalDate startDate, LocalDate endDate, int recruitCount, Long bookId, Long categoryId) { + validateVisibilityPasswordRule(isPublic, password); + + return Room.builder() + .id(null) + .title(title) + .description(description) + .isPublic(isPublic) + .password(password) + .roomPercentage(roomPercentage) + .startDate(startDate) + .endDate(endDate) + .recruitCount(recruitCount) + .bookId(bookId) + .categoryId(categoryId) + .build(); + } + + private static void validateVisibilityPasswordRule(boolean isPublic, String password) { + boolean hasPassword = password != null; + + if ((isPublic && hasPassword) || (!isPublic && !hasPassword)) { + String message = String.format( + "방 공개/비공개 여부와 비밀번호 설정이 일치하지 않습니다. 공개 여부 = %s, 비밀번호 존재 여부 = %s", + isPublic, hasPassword + ); + throw new InvalidStateException(INVALID_ROOM_CREATE, + new IllegalArgumentException(message)); + } + } } From 7d1dfa48620956f87f4d4e4ce5a0809b84ffd219 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:01:24 +0900 Subject: [PATCH 03/25] =?UTF-8?q?[feat]=20BookApiQueryPort=20=EC=97=90=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=8B=9C=EA=B7=B8=EB=8B=88?= =?UTF-8?q?=EC=B2=98=20=EC=B6=94=EA=B0=80=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생성할 방과 연관된 책의 pageCount 값을 로드하기 위한 메서드 시그니처 추가 --- .../thip/book/application/port/out/BookApiQueryPort.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java index 1bbf8fdc4..06917d108 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java @@ -2,8 +2,13 @@ import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.domain.Book; public interface BookApiQueryPort { NaverBookParseResult findBooksByKeyword(String keyword, int start); NaverDetailBookParseResult findDetailBookByKeyword(String isbn); + + Integer findPageCountByIsbn(String isbn); + + Book loadBookWithPageByIsbn(String isbn); } From 1856c5541b4f50699ab845a3f79a0a273cf6ae25 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:04:20 +0900 Subject: [PATCH 04/25] =?UTF-8?q?[feat]=20facade=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=9C=20BookApiQueryPort?= =?UTF-8?q?=EC=9D=98=20=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CompositionBookApiAdapter 내부에서 네이버, 알라딘 api와의 통신 결과를 결합할 수 있도록 api adapter 구성 --- .../out/api/BookApiNaverApiAdapter.java | 4 +- .../out/api/CompositeBookApiAdapter.java | 53 +++++++++++++++++++ .../out/api/aladin/AladinApiClient.java | 15 ++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java create mode 100644 src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java index 3a2eedfe8..ae0a30f24 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java @@ -8,17 +8,15 @@ @Component @RequiredArgsConstructor -public class BookApiNaverApiAdapter implements BookApiQueryPort { +public class BookApiNaverApiAdapter { private final NaverApiUtil naverApiUtil; - @Override public NaverBookParseResult findBooksByKeyword(String keyword, int start) { String xml = naverApiUtil.searchBook(keyword, start); // 네이버 API 호출 return NaverBookXmlParser.parseBookList(xml); // XML 파싱 + 페이징 정보 포함 } - @Override public NaverDetailBookParseResult findDetailBookByKeyword(String isbn) { String xml = naverApiUtil.detailSearchBook(isbn); // 네이버 API 호출 return NaverBookXmlParser.parseBookDetail(xml); // XML 파싱 diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java new file mode 100644 index 000000000..9af44e994 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java @@ -0,0 +1,53 @@ +package konkuk.thip.book.adapter.out.api; + +import konkuk.thip.book.adapter.out.api.aladin.AladinApiClient; +import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.domain.Book; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CompositeBookApiAdapter implements BookApiQueryPort { + + private final BookApiNaverApiAdapter bookApiNaverApiAdapter; + private final AladinApiClient aladinApiClient; + + @Override + public NaverBookParseResult findBooksByKeyword(String keyword, int start) { + return bookApiNaverApiAdapter.findBooksByKeyword(keyword, start); + } + + @Override + public NaverDetailBookParseResult findDetailBookByKeyword(String isbn) { + return bookApiNaverApiAdapter.findDetailBookByKeyword(isbn); + } + + @Override + public Integer findPageCountByIsbn(String isbn) { + return aladinApiClient.findPageCountByIsb(isbn); + } + + @Override + public Book loadBookWithPageByIsbn(String isbn) { + // 1. naver 상세정보 조회 api 로 책 상세정보(without page) load + NaverDetailBookParseResult detailBookByKeyword = findDetailBookByKeyword(isbn); + + // 2. 알라딘으로부터 책 page 정보 load + Integer pageCount = findPageCountByIsbn(isbn); + + // 3. pageCount 정보를 포함한 Book 반환 + return Book.withoutId( + detailBookByKeyword.title(), + isbn, + detailBookByKeyword.author(), + false, + detailBookByKeyword.publisher(), + detailBookByKeyword.imageUrl(), + pageCount, + detailBookByKeyword.description() + ); + } +} diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java new file mode 100644 index 000000000..6b4402724 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java @@ -0,0 +1,15 @@ +package konkuk.thip.book.adapter.out.api.aladin; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AladinApiClient { + + private final AladinApiUtil aladinApiUtil; + + public Integer findPageCountByIsb(String isbn) { + return aladinApiUtil.getPageCount(isbn); + } +} From 4aca1e7cc56590e8140a860715d953d89295b4f6 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:05:43 +0900 Subject: [PATCH 05/25] =?UTF-8?q?[feat]=20=EC=95=8C=EB=9D=BC=EB=94=98=20op?= =?UTF-8?q?en=20api=EC=99=80=EC=9D=98=20=ED=86=B5=EC=8B=A0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - restTemplate 활용하여 통신하도록 구현 --- .../adapter/out/api/aladin/AladinApiUtil.java | 65 +++++++++++++++++++ .../thip/config/RestTemplateConfig.java | 14 ++++ 2 files changed, 79 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java create mode 100644 src/main/java/konkuk/thip/config/RestTemplateConfig.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java new file mode 100644 index 000000000..d82c64af6 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java @@ -0,0 +1,65 @@ +package konkuk.thip.book.adapter.out.api.aladin; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; + +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_ISBN_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_PARSING_ERROR; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AladinApiUtil { + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + @Value("${aladin.ttbKey}") + private String ttbKey; + + private static final String BASE_URL = "http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?"; + private static final String ITEM_ID_TYPE = "ISBN"; + private static final String OUTPUT = "js"; + private static final String API_VERSION = "20131101"; + private static final String SUB_INFO_PARSING_KEY = "subInfo"; + private static final String PAGE_COUNT_PARSING_KEY = "itemPage"; + + private String buildLookupUrl(String isbn) { + return String.format( + BASE_URL + "ttbkey=%s&itemIdType=%s&itemId=%s&output=%s&Version=%s", + ttbKey, ITEM_ID_TYPE, isbn, OUTPUT, API_VERSION + ); + } + + public Integer getPageCount(String isbn) { + String url = buildLookupUrl(isbn); + String response = restTemplate.getForObject(url, String.class); + + try { + JsonNode jsonNode = objectMapper.readTree(response); + JsonNode items = jsonNode.path("item"); + + // json 응답 결과에 item 키값이 없는 경우 + if (!items.isArray() || items.isEmpty()) { + // TODO : 알라딘으로부터 page 정보가 없으면 ?? + // 보상 시나리오 : 유저에게 "page 정보를 찾을 수 없는 책입니다. 직접 page 정보를 입력하세요" 라고 안내 + // 일단 지금은 exception throw 만 진행 + throw new BusinessException(BOOK_ALADIN_API_ISBN_NOT_FOUND); + } + + JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY); + + return subInfo.path(PAGE_COUNT_PARSING_KEY).asInt(); + } catch (IOException e) { + throw new BusinessException(BOOK_ALADIN_API_PARSING_ERROR); + } + } +} diff --git a/src/main/java/konkuk/thip/config/RestTemplateConfig.java b/src/main/java/konkuk/thip/config/RestTemplateConfig.java new file mode 100644 index 000000000..11b10d449 --- /dev/null +++ b/src/main/java/konkuk/thip/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package konkuk.thip.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} From 41fdad15f3e7b7b435381a8e5c109d0b560baa3f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:07:41 +0900 Subject: [PATCH 06/25] =?UTF-8?q?[feat]=20value=20=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20Category=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CategoryCommandPersistenceAdapter.java | 28 +++++++++++++++++++ .../persistence/CategoryJpaRepository.java | 4 +++ .../port/out/CategoryCommandPort.java | 8 ++++++ 3 files changed, 40 insertions(+) create mode 100644 src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryCommandPersistenceAdapter.java create mode 100644 src/main/java/konkuk/thip/room/application/port/out/CategoryCommandPort.java diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryCommandPersistenceAdapter.java new file mode 100644 index 000000000..ae2e25aa6 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryCommandPersistenceAdapter.java @@ -0,0 +1,28 @@ +package konkuk.thip.room.adapter.out.persistence; + +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.mapper.CategoryMapper; +import konkuk.thip.room.application.port.out.CategoryCommandPort; +import konkuk.thip.room.domain.Category; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import static konkuk.thip.common.exception.code.ErrorCode.CATEGORY_NOT_FOUND; + +@Repository +@RequiredArgsConstructor +public class CategoryCommandPersistenceAdapter implements CategoryCommandPort { + + private final CategoryJpaRepository categoryJpaRepository; + private final CategoryMapper categoryMapper; + + @Override + public Category findByValue(String value) { + CategoryJpaEntity categoryJpaEntity = categoryJpaRepository.findByValue(value).orElseThrow( + () -> new EntityNotFoundException(CATEGORY_NOT_FOUND) + ); + + return categoryMapper.toDomainEntity(categoryJpaEntity); + } +} diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryJpaRepository.java index 2cc722535..a8c9c04ef 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/CategoryJpaRepository.java @@ -3,5 +3,9 @@ import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CategoryJpaRepository extends JpaRepository { + + Optional findByValue(String value); } diff --git a/src/main/java/konkuk/thip/room/application/port/out/CategoryCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/CategoryCommandPort.java new file mode 100644 index 000000000..5aa68ff5a --- /dev/null +++ b/src/main/java/konkuk/thip/room/application/port/out/CategoryCommandPort.java @@ -0,0 +1,8 @@ +package konkuk.thip.room.application.port.out; + +import konkuk.thip.room.domain.Category; + +public interface CategoryCommandPort { + + Category findByValue(String value); +} From 8b779b03d2f24ae4fad6f68e2f749bcc8b3ab44d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:09:29 +0900 Subject: [PATCH 07/25] =?UTF-8?q?[feat]=20Book=20pageCount=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20update=20=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/jpa/BookJpaEntity.java | 5 ++++- .../out/persistence/BookCommandPersistenceAdapter.java | 10 ++++++++++ .../book/application/port/out/BookCommandPort.java | 4 +++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/jpa/BookJpaEntity.java b/src/main/java/konkuk/thip/book/adapter/out/jpa/BookJpaEntity.java index 62c57cba0..6fe64a38a 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/jpa/BookJpaEntity.java +++ b/src/main/java/konkuk/thip/book/adapter/out/jpa/BookJpaEntity.java @@ -39,4 +39,7 @@ public class BookJpaEntity extends BaseJpaEntity { @Column(length = 1000) private String description; -} \ No newline at end of file + public void changePageCount(Integer pageCount) { + this.pageCount = pageCount; + } +} diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 34f572296..8605655e5 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -39,4 +39,14 @@ public Book findById(Long id) { return bookMapper.toDomainEntity(bookJpaEntity); } + + @Override + public void updateForPageCount(Book book) { + BookJpaEntity bookJpaEntity = bookJpaRepository.findById(book.getId()).orElseThrow( + () -> new EntityNotFoundException(BOOK_NOT_FOUND) + ); + + bookJpaEntity.changePageCount(book.getPageCount()); + bookJpaRepository.save(bookJpaEntity); + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index a1c83f9d8..c7ce10e8e 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -10,4 +10,6 @@ public interface BookCommandPort { Long save(Book book); Book findById(Long id); -} \ No newline at end of file + + void updateForPageCount(Book book); +} From 7acbb2c139d0fe6e9d19ad5d1f7581e9c47638ed Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:10:11 +0900 Subject: [PATCH 08/25] =?UTF-8?q?[feat]=20error=20code=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/adapter/out/api/NaverBookXmlParser.java | 4 ++-- .../thip/common/exception/code/ErrorCode.java | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java b/src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java index ec3e1144e..faca3a9f2 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java @@ -11,7 +11,7 @@ import java.util.List; import org.xml.sax.InputSource; -import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ISBN_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NAVER_API_ISBN_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NAVER_API_PARSING_ERROR; public class NaverBookXmlParser { @@ -57,7 +57,7 @@ public static NaverDetailBookParseResult parseBookDetail(String xml) { if (totalStr != null) total = Integer.parseInt(totalStr); // total이 0이면 isbn에 해당하는 책이 없음(잘못 넘어온 isbn 예외처리) - if (total == 0) throw new BusinessException(BOOK_ISBN_NOT_FOUND); + if (total == 0) throw new BusinessException(BOOK_NAVER_API_ISBN_NOT_FOUND); List items = getItemElements(channel); if (!items.isEmpty()) { diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 5de2c7ec8..b9d04d42a 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -40,37 +40,44 @@ public enum ErrorCode implements ResponseCode { */ BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.BAD_REQUEST, 80000, "검색어 인코딩에 실패했습니다."), BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."), - BOOK_NAVER_API_PARSING_ERROR(HttpStatus.BAD_REQUEST, 80002,"네이버 API 응답 파싱에 실패하였습니다."), + BOOK_NAVER_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80002,"네이버 API 응답 파싱에 실패하였습니다."), BOOK_NAVER_API_URL_ERROR(HttpStatus.BAD_REQUEST, 80003,"네이버 API URL이 잘못되었습니다."), BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."), BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80005,"네이버 API 응답에 실패하였습니다."), BOOK_SEARCH_PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, 80006,"검색어 페이지가 범위를 벗어났습니다."), BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), - BOOK_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "ISBN으로 검색한 결과가 존재하지 않습니다."), + BOOK_NAVER_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "네이버 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."), BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."), DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."), BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."), BOOK_NOT_SAVED_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."), + BOOK_ALADIN_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80015, "알라딘 API 응답 파싱에 실패하였습니다."), + BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."), /** * 90000 : recentSearch error */ INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, 900000,"알맞은 검색어 타입을 찾을 수 없습니다."), - /** * 100000 : room error */ ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 100000, "존재하지 않는 ROOM 입니다."), + INVALID_ROOM_CREATE(HttpStatus.BAD_REQUEST, 100001, "유효하지 않은 ROOM 생성 요청 입니다."), /** * 110000 : vote error */ VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."), VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다."), - INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다.") + INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다."), + + /** + * 120000 : Category error + */ + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, 120000, "존재하지 않는 CATEGORY 입니다.") ; From b8b9ce73c0f97e28312fc0d634b4218e3555a4f3 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:11:00 +0900 Subject: [PATCH 09/25] =?UTF-8?q?[feat]=20Room=20save=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/adapter/out/jpa/RoomJpaEntity.java | 2 +- .../room/adapter/out/mapper/RoomMapper.java | 2 +- .../RoomCommandPersistenceAdapter.java | 22 ++++++++++++++++++- .../application/port/out/RoomCommandPort.java | 2 ++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index ce12c70fe..5426f28e2 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -29,7 +29,7 @@ public class RoomJpaEntity extends BaseJpaEntity { private boolean isPublic; @Column(length = 4) - private Integer password; + private String password; @Builder.Default @Column(name = "room_percentage",nullable = false) diff --git a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java index e28e2d573..9196e7f8d 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java +++ b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java @@ -9,7 +9,7 @@ @Component public class RoomMapper { - public RoomJpaEntity roomJpaEntity(Room room, BookJpaEntity bookJpaEntity, CategoryJpaEntity categoryJpaEntity) { + public RoomJpaEntity toJpaEntity(Room room, BookJpaEntity bookJpaEntity, CategoryJpaEntity categoryJpaEntity) { return RoomJpaEntity.builder() .title(room.getTitle()) .description(room.getDescription()) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java index c6a607187..e86795d08 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java @@ -1,6 +1,9 @@ package konkuk.thip.room.adapter.out.persistence; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.mapper.RoomMapper; import konkuk.thip.room.application.port.out.RoomCommandPort; @@ -8,13 +11,16 @@ 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.*; @Repository @RequiredArgsConstructor public class RoomCommandPersistenceAdapter implements RoomCommandPort { private final RoomJpaRepository roomJpaRepository; + private final BookJpaRepository bookJpaRepository; + private final CategoryJpaRepository categoryJpaRepository; + private final RoomMapper roomMapper; @Override @@ -25,4 +31,18 @@ public Room findById(Long id) { return roomMapper.toDomainEntity(roomJpaEntity); } + + @Override + public Long save(Room room) { + BookJpaEntity bookJpaEntity = bookJpaRepository.findById(room.getBookId()).orElseThrow( + () -> new EntityNotFoundException(BOOK_NOT_FOUND) + ); + + CategoryJpaEntity categoryJpaEntity = categoryJpaRepository.findById(room.getCategoryId()).orElseThrow( + () -> new EntityNotFoundException(CATEGORY_NOT_FOUND) + ); + + RoomJpaEntity roomJpaEntity = roomMapper.toJpaEntity(room, bookJpaEntity, categoryJpaEntity); + return roomJpaRepository.save(roomJpaEntity).getRoomId(); + } } diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java index aa4ca8d28..57cdafd88 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java @@ -5,4 +5,6 @@ public interface RoomCommandPort { Room findById(Long id); + + Long save(Room room); } From 8235fa184f14a37e5232be9d7ffb2af4535bfa05 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:11:24 +0900 Subject: [PATCH 10/25] =?UTF-8?q?[feat]=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api=20controller=20=EA=B0=9C=EB=B0=9C=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/RoomCommandController.java | 15 +++++ .../adapter/in/web/request/DummyRequest.java | 7 --- .../in/web/request/RoomCreateRequest.java | 59 +++++++++++++++++++ .../in/web/response/DummyResponse.java | 7 --- .../in/web/response/RoomCreateResponse.java | 7 +++ 5 files changed, 81 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/request/DummyRequest.java create mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java delete mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/DummyResponse.java create mode 100644 src/main/java/konkuk/thip/room/adapter/in/web/response/RoomCreateResponse.java diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java index 90ffc6982..dc09f4b5c 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java @@ -1,10 +1,25 @@ package konkuk.thip.room.adapter.in.web; +import jakarta.validation.Valid; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.room.adapter.in.web.request.RoomCreateRequest; +import konkuk.thip.room.adapter.in.web.response.RoomCreateResponse; +import konkuk.thip.room.application.port.in.RoomCreateUseCase; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class RoomCommandController { + private final RoomCreateUseCase roomCreateUseCase; + + @PostMapping("/rooms") + public BaseResponse createRoom(@Valid @RequestBody RoomCreateRequest request) { + return BaseResponse.ok(RoomCreateResponse.of( + roomCreateUseCase.createRoom(request.toCommand()) + )); + } } diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/DummyRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/DummyRequest.java deleted file mode 100644 index 2e023b5fa..000000000 --- a/src/main/java/konkuk/thip/room/adapter/in/web/request/DummyRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.room.adapter.in.web.request; - -import lombok.Getter; - -@Getter -public class DummyRequest { -} diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java new file mode 100644 index 000000000..6b8759472 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java @@ -0,0 +1,59 @@ +package konkuk.thip.room.adapter.in.web.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.*; +import konkuk.thip.room.application.port.in.dto.RoomCreateCommand; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public record RoomCreateRequest( + @NotBlank(message = "ISBN은 필수입니다.") + String isbn, + + @NotBlank(message = "카테고리는 필수입니다.") + String category, + + @NotBlank(message = "방 이름은 필수입니다.") + String roomName, + + @NotBlank(message = "설명은 필수입니다.") + String description, + + @Pattern( + regexp = "\\d{4}\\.\\d{2}\\.\\d{2}", + message = "진행 시작일은 yyyy.MM.dd 형식이어야 합니다." + ) + String progressStartDate, + + @Pattern( + regexp = "\\d{4}\\.\\d{2}\\.\\d{2}", + message = "진행 종료일은 yyyy.MM.dd 형식이어야 합니다." + ) + String progressEndDate, + + @Min(value = 1, message = "모집 인원은 최소 1명이어야 합니다.") + int recruitCount, + + @Nullable + @Pattern(regexp = "\\d{4}", message = "비밀번호는 숫자 4자리여야 합니다.") + String password, + + @NotNull(message = "방 공개 설정 여부는 필수입니다.") + Boolean isPublic +) { + public RoomCreateCommand toCommand() { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + return new RoomCreateCommand( + isbn, + category, + roomName, + description, + LocalDate.parse(progressStartDate, formatter), + LocalDate.parse(progressEndDate, formatter), + recruitCount, + password, + isPublic + ); + } +} diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/DummyResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/DummyResponse.java deleted file mode 100644 index 59f496684..000000000 --- a/src/main/java/konkuk/thip/room/adapter/in/web/response/DummyResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.room.adapter.in.web.response; - -import lombok.Getter; - -@Getter -public class DummyResponse { -} diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomCreateResponse.java b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomCreateResponse.java new file mode 100644 index 000000000..12489b279 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/in/web/response/RoomCreateResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.adapter.in.web.response; + +public record RoomCreateResponse(Long roomId) { + public static RoomCreateResponse of(Long roomId) { + return new RoomCreateResponse(roomId); + } +} From 538b12ce12396e3d61a64cd9a7f02767be60370d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:11:41 +0900 Subject: [PATCH 11/25] =?UTF-8?q?[test]=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#4?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/RoomCreateAPITest.java | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateAPITest.java diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateAPITest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateAPITest.java new file mode 100644 index 000000000..6e9623cc7 --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateAPITest.java @@ -0,0 +1,282 @@ +package konkuk.thip.room.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("방 생성 api 통합 테스트") +class RoomCreateAPITest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @AfterEach + void tearDown() { + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + private void saveUserAndCategory() { + AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() + .value("책벌레") + .color("blue") + .imageUrl("http://image.url") + .build()); + + userJpaRepository.save(UserJpaEntity.builder() + .oauth2Id("kakao_432708231") + .nickname("User1") + .imageUrl("https://avatar1.jpg") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()); + + categoryJpaRepository.save(CategoryJpaEntity.builder() + .value("소설") + .aliasForCategoryJpaEntity(alias) + .build()); + } + + private void saveBookWithPageCount() { + bookJpaRepository.save(BookJpaEntity.builder() + .title("작별하지 않는다") + .isbn("9788954682152") // 실제 isbn 값 + .authorName("한강") + .bestSeller(false) + .publisher("문학동네") + .imageUrl("https://image1.jpg") + .pageCount(332) // pageCount 값이 null이 아닌 책 + .description("한강의 소설") + .build()); + } + + private void saveBookWithoutPageCount() { + bookJpaRepository.save(BookJpaEntity.builder() + .title("작별하지 않는다") + .isbn("9788954682152") // 실제 isbn 값 + .authorName("한강") + .bestSeller(false) + .publisher("문학동네") + .imageUrl("https://image1.jpg") + .pageCount(null) // pageCount 값이 null 인 책 -> 실제 페이지 정보 332 + .description("한강의 소설") + .build()); + } + + private Map buildRoomCreateRequest() { + Map request = new HashMap<>(); + request.put("isbn", "9788954682152"); + request.put("category", "소설"); + request.put("roomName", "방이름"); + request.put("description", "방설명"); + request.put("progressStartDate", "2025.07.10"); + request.put("progressEndDate", "2025.08.10"); + request.put("recruitCount", 3); + request.put("password", null); + request.put("isPublic", true); + return request; + } + + @Test + @DisplayName("isbn 에 해당하는 책(with pageCount)이 DB에 존재할 때, 해당 책과 연관된 방을 생성할 수 있다.") + void room_create_book_with_page_exist() throws Exception { + //given : user, category, pageCount값이 있는 book 생성, request 생성 + saveUserAndCategory(); + saveBookWithPageCount(); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + Long bookId = bookJpaRepository.findAll().get(0).getBookId(); + Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId(); + + Map request = buildRoomCreateRequest(); + + //when + ResultActions result = mockMvc.perform(post("/rooms") + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request) + )); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + Long roomId = jsonNode.path("data").path("roomId").asLong(); + + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(roomId).orElse(null); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + LocalDate startDate = LocalDate.parse((String) request.get("progressStartDate"), formatter); + LocalDate endDate = LocalDate.parse((String) request.get("progressEndDate"), formatter); + + assertThat(roomJpaEntity).isNotNull() + .extracting( + "title", "description", "public", "password", "roomPercentage", + "startDate", "endDate", "recruitCount", "bookJpaEntity.bookId", "categoryJpaEntity.categoryId" + ) + .containsExactly( + request.get("roomName"), request.get("description"), request.get("isPublic"), request.get("password"), 0.0, + startDate, endDate, request.get("recruitCount"), bookId, categoryId + ); + } + + @Test + @DisplayName("isbn 에 해당하는 책(without pageCount)이 DB에 존재할 때, 해당 책의 page 정보를 update 한 후 연관된 방을 생성할 수 있다.") + void room_create_book_without_page_exist() throws Exception { + //given : user, category, pageCount값이 없는 book 생성, request 생성 + saveUserAndCategory(); + saveBookWithoutPageCount(); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + Long bookId = bookJpaRepository.findAll().get(0).getBookId(); + Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId(); + + Map request = buildRoomCreateRequest(); + + //when + ResultActions result = mockMvc.perform(post("/rooms") + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request) + )); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + Long roomId = jsonNode.path("data").path("roomId").asLong(); + + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(roomId).orElse(null); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + LocalDate startDate = LocalDate.parse((String) request.get("progressStartDate"), formatter); + LocalDate endDate = LocalDate.parse((String) request.get("progressEndDate"), formatter); + + assertThat(roomJpaEntity).isNotNull() + .extracting( + "title", "description", "public", "password", "roomPercentage", + "startDate", "endDate", "recruitCount", "bookJpaEntity.bookId", "categoryJpaEntity.categoryId" + ) + .containsExactly( + request.get("roomName"), request.get("description"), request.get("isPublic"), request.get("password"), 0.0, + startDate, endDate, request.get("recruitCount"), bookId, categoryId + ); + + // update 된 책 검증 + BookJpaEntity updatedBookJpaEntity = bookJpaRepository.findById(bookId).orElse(null); + assertThat(updatedBookJpaEntity.getPageCount()).isEqualTo(332); + } + + @Test + @DisplayName("isbn 에 해당하는 책이 존재하지 않을 경우, page 정보를 포함하는 책을 save 한 후 연관된 방을 생성할 수 있다.") + void room_create_book_not_exist() throws Exception { + //given : user, category 생성, request 생성 (book 생성 X) + saveUserAndCategory(); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + Long categoryId = categoryJpaRepository.findAll().get(0).getCategoryId(); + + Map request = buildRoomCreateRequest(); + + //when + ResultActions result = mockMvc.perform(post("/rooms") + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request) + )); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.roomId").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + Long roomId = jsonNode.path("data").path("roomId").asLong(); + + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(roomId).orElse(null); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd"); + LocalDate startDate = LocalDate.parse((String) request.get("progressStartDate"), formatter); + LocalDate endDate = LocalDate.parse((String) request.get("progressEndDate"), formatter); + + assertThat(roomJpaEntity).isNotNull() + .extracting( + "title", "description", "public", "password", "roomPercentage", + "startDate", "endDate", "recruitCount", "categoryJpaEntity.categoryId" + ) + .containsExactly( + request.get("roomName"), request.get("description"), request.get("isPublic"), request.get("password"), 0.0, + startDate, endDate, request.get("recruitCount"), categoryId + ); + + // 새로 DB에 저장된 책 검증 + BookJpaEntity newSavedBookJpaEntity = bookJpaRepository.findAll().get(0); + assertThat(newSavedBookJpaEntity) + .extracting( + "isbn", "authorName", "pageCount" + ) + .containsExactly( + "9788954682152", "한강", 332 + ); + } + +} From 54139ee749aecb6b61aa0d8c290948a3bb7d7210 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:12:18 +0900 Subject: [PATCH 12/25] =?UTF-8?q?[test]=20=EB=B0=A9=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20api=20controller=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @Nested 어노테이션 활용해서 테스트코드 모듈화 진행 --- .../in/web/RoomCreateControllerTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java new file mode 100644 index 000000000..d4da2db50 --- /dev/null +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java @@ -0,0 +1,187 @@ +package konkuk.thip.room.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("방 생성 api controller 단위 테스트") +class RoomCreateControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + + private Map buildValidRequest() { + Map request = new HashMap<>(); + request.put("isbn", "9788954682152"); + request.put("category", "소설"); + request.put("roomName", "방이름"); + request.put("description", "방설명"); + request.put("progressStartDate", "2025.07.10"); + request.put("progressEndDate", "2025.08.10"); + request.put("recruitCount", 3); + request.put("password", null); + request.put("isPublic", true); + return request; + } + + private void assertBad(Map req, String msg) throws Exception { + mockMvc.perform(post("/rooms") + .requestAttr("userId", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString(msg))); + } + + @Nested + @DisplayName("ISBN 검증") + class IsbnValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_isbn() throws Exception { + Map req = buildValidRequest(); + req.put("isbn", ""); + assertBad(req, "ISBN은 필수입니다."); + } + } + + @Nested + @DisplayName("Category 검증") + class CategoryValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_category() throws Exception { + Map req = buildValidRequest(); + req.put("category", ""); + assertBad(req, "카테고리는 필수입니다."); + } + } + + @Nested + @DisplayName("RoomName 검증") + class RoomNameValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_room_name() throws Exception { + Map req = buildValidRequest(); + req.put("roomName", ""); + assertBad(req, "방 이름은 필수입니다."); + } + } + + @Nested + @DisplayName("Description 검증") + class DescriptionValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_description() throws Exception { + Map req = buildValidRequest(); + req.put("description", ""); + assertBad(req, "설명은 필수입니다."); + } + } + + @Nested + @DisplayName("StartDate 검증") + class StartDateValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_start_date() throws Exception { + Map req = buildValidRequest(); + req.put("progressStartDate", ""); + assertBad(req, "진행 시작일은 yyyy.MM.dd 형식이어야 합니다."); + } + @Test + @DisplayName("형식 벗어날 때 400 error") + void pattern_start_date() throws Exception { + Map req = buildValidRequest(); + req.put("progressStartDate", "2025-07-10"); + assertBad(req, "진행 시작일은 yyyy.MM.dd 형식이어야 합니다."); + } + } + + @Nested + @DisplayName("EndDate 검증") + class EndDateValidation { + @Test + @DisplayName("빈 문자열일 때 400 error") + void blank_end_date() throws Exception { + Map req = buildValidRequest(); + req.put("progressEndDate", ""); + assertBad(req, "진행 종료일은 yyyy.MM.dd 형식이어야 합니다."); + } + @Test + @DisplayName("형식 벗어날 때 400 error") + void pattern_end_date() throws Exception { + Map req = buildValidRequest(); + req.put("progressEndDate", "2025/08/10"); + assertBad(req, "진행 종료일은 yyyy.MM.dd 형식이어야 합니다."); + } + } + + @Nested + @DisplayName("RecruitCount 검증") + class RecruitCountValidation { + @Test + @DisplayName("1 미만일 때 400 error") + void less_than_one() throws Exception { + Map req = buildValidRequest(); + req.put("recruitCount", 0); + assertBad(req, "모집 인원은 최소 1명이어야 합니다."); + } + } + + @Nested + @DisplayName("Password 검증") + class PasswordValidation { + @Test + @DisplayName("숫자로 구성되지 않았을 때 400 error") + void invalid_password() throws Exception { + Map req = buildValidRequest(); + req.put("password", "12ab"); + assertBad(req, "비밀번호는 숫자 4자리여야 합니다."); + } + + @Test + @DisplayName("4자리 숫자 아닐 때 400 error") + void short_password() throws Exception { + Map req = buildValidRequest(); + req.put("password", "123"); + assertBad(req, "비밀번호는 숫자 4자리여야 합니다."); + } + } + + @Nested + @DisplayName("isPublic 검증") + class IsPublicValidation { + @Test + @DisplayName("값이 없을 때 400 error") + void missing_is_public() throws Exception { + Map req = buildValidRequest(); + req.put("isPublic", null); + assertBad(req, "방 공개 설정 여부는 필수입니다."); + } + } +} From e106de59278eebc4fa731040fb6a1e728aa521bc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 6 Jul 2025 19:14:12 +0900 Subject: [PATCH 13/25] =?UTF-8?q?[test]=20Room=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 방 공개 여부, 비밀번호 설정 여부의 일관성을 검증한 후, 방 생성 진행할 수 있도록 테스트 코드 작성 --- .../konkuk/thip/room/domain/RoomTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/test/java/konkuk/thip/room/domain/RoomTest.java diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java new file mode 100644 index 000000000..71f06fb6e --- /dev/null +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -0,0 +1,44 @@ +package konkuk.thip.room.domain; + +import konkuk.thip.common.exception.InvalidStateException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("Room 단위 테스트") +class RoomTest { + + private final LocalDate START = LocalDate.of(2025, 7, 1); + private final LocalDate END = LocalDate.of(2025, 8, 1); + + @Test + @DisplayName("withoutId: 공개 방이면서 password가 not null 이면, InvalidStateException 발생한다.") + void withoutId_public_password_not_null() { + InvalidStateException ex = assertThrows(InvalidStateException.class, () -> + Room.withoutId( + "제목", "설명", true, "1234", + 0.0, START, END, 5, 123L, 456L + ) + ); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("방 공개/비공개 여부와 비밀번호 설정이 일치하지 않습니다.")); + assertTrue(ex.getCause().getMessage().contains("공개 여부 = true, 비밀번호 존재 여부 = true")); + } + + @Test + @DisplayName("withoutId: 비공개 방이면서 password가 null 이면, InvalidStateException 발생한다.") + void withoutId_private_password_null() { + InvalidStateException ex = assertThrows(InvalidStateException.class, () -> + Room.withoutId( + "제목", "설명", false, null, + 0.0, START, END, 5, 123L, 456L + ) + ); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("방 공개/비공개 여부와 비밀번호 설정이 일치하지 않습니다.")); + assertTrue(ex.getCause().getMessage().contains("공개 여부 = false, 비밀번호 존재 여부 = false")); + } +} From ca99132d392baad7280d3ea46314ccddf9a5d955 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 15:27:29 +0900 Subject: [PATCH 14/25] =?UTF-8?q?[refactor]=20Naver=20api=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=20=EA=B0=9D=EC=B2=B4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/adapter/out/api/CompositeBookApiAdapter.java | 10 +++++----- ...BookApiNaverApiAdapter.java => NaverApiClient.java} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/konkuk/thip/book/adapter/out/api/{BookApiNaverApiAdapter.java => NaverApiClient.java} (95%) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java index 9af44e994..e6e25f989 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java @@ -12,17 +12,17 @@ @RequiredArgsConstructor public class CompositeBookApiAdapter implements BookApiQueryPort { - private final BookApiNaverApiAdapter bookApiNaverApiAdapter; + private final NaverApiClient naverApiClient; private final AladinApiClient aladinApiClient; @Override public NaverBookParseResult findBooksByKeyword(String keyword, int start) { - return bookApiNaverApiAdapter.findBooksByKeyword(keyword, start); + return naverApiClient.findBooksByKeyword(keyword, start); } @Override - public NaverDetailBookParseResult findDetailBookByKeyword(String isbn) { - return bookApiNaverApiAdapter.findDetailBookByKeyword(isbn); + public NaverDetailBookParseResult findDetailBookByIsbn(String isbn) { + return naverApiClient.findDetailBookByIsbn(isbn); } @Override @@ -33,7 +33,7 @@ public Integer findPageCountByIsbn(String isbn) { @Override public Book loadBookWithPageByIsbn(String isbn) { // 1. naver 상세정보 조회 api 로 책 상세정보(without page) load - NaverDetailBookParseResult detailBookByKeyword = findDetailBookByKeyword(isbn); + NaverDetailBookParseResult detailBookByKeyword = findDetailBookByIsbn(isbn); // 2. 알라딘으로부터 책 page 정보 load Integer pageCount = findPageCountByIsbn(isbn); diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/NaverApiClient.java similarity index 95% rename from src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java rename to src/main/java/konkuk/thip/book/adapter/out/api/NaverApiClient.java index 470dbf333..a66fdf175 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/NaverApiClient.java @@ -7,7 +7,7 @@ @Component @RequiredArgsConstructor -public class BookApiNaverApiAdapter { +public class NaverApiClient { private final NaverApiUtil naverApiUtil; From 3349ae6c6864b6b97de11d32d5df3ee271726a2f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 15:27:49 +0900 Subject: [PATCH 15/25] =?UTF-8?q?[refactor]=20AladinApiClient=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/api/CompositeBookApiAdapter.java | 2 +- .../thip/book/adapter/out/api/aladin/AladinApiClient.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java index e6e25f989..ddb95eb45 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java @@ -27,7 +27,7 @@ public NaverDetailBookParseResult findDetailBookByIsbn(String isbn) { @Override public Integer findPageCountByIsbn(String isbn) { - return aladinApiClient.findPageCountByIsb(isbn); + return aladinApiClient.findPageCountByIsbn(isbn); } @Override diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java index 6b4402724..3b6b12abd 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java @@ -9,7 +9,7 @@ public class AladinApiClient { private final AladinApiUtil aladinApiUtil; - public Integer findPageCountByIsb(String isbn) { + public Integer findPageCountByIsbn(String isbn) { return aladinApiUtil.getPageCount(isbn); } } From 77852a848caa56432220e8eac27051dfe4067c8e Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 15:42:17 +0900 Subject: [PATCH 16/25] =?UTF-8?q?[refactor]=20AladinApiUtil=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20enum=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/api/aladin/AladinApiParam.java | 20 +++++++++++++++++ .../adapter/out/api/aladin/AladinApiUtil.java | 22 ++++++++++--------- 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java new file mode 100644 index 000000000..a6c51c27a --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java @@ -0,0 +1,20 @@ +package konkuk.thip.book.adapter.out.api.aladin; + +public enum AladinApiParam { + + ITEM_ID_TYPE("ISBN"), + OUTPUT("js"), + API_VERSION("20131101"), + SUB_INFO_PARSING_KEY("subInfo"), + PAGE_COUNT_PARSING_KEY("itemPage"); + + private final String value; + + AladinApiParam(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java index d82c64af6..7a63ac018 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java @@ -11,6 +11,7 @@ import java.io.IOException; +import static konkuk.thip.book.adapter.out.api.aladin.AladinApiParam.*; import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_ISBN_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_PARSING_ERROR; @@ -25,17 +26,18 @@ public class AladinApiUtil { @Value("${aladin.ttbKey}") private String ttbKey; - private static final String BASE_URL = "http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?"; - private static final String ITEM_ID_TYPE = "ISBN"; - private static final String OUTPUT = "js"; - private static final String API_VERSION = "20131101"; - private static final String SUB_INFO_PARSING_KEY = "subInfo"; - private static final String PAGE_COUNT_PARSING_KEY = "itemPage"; + @Value("${aladin.baseUrl}") + private String baseUrl; + private String buildLookupUrl(String isbn) { return String.format( - BASE_URL + "ttbkey=%s&itemIdType=%s&itemId=%s&output=%s&Version=%s", - ttbKey, ITEM_ID_TYPE, isbn, OUTPUT, API_VERSION + baseUrl + "ttbkey=%s&itemIdType=%s&itemId=%s&output=%s&Version=%s", + ttbKey, + ITEM_ID_TYPE.getValue(), + isbn, + OUTPUT.getValue(), + API_VERSION.getValue() ); } @@ -55,9 +57,9 @@ public Integer getPageCount(String isbn) { throw new BusinessException(BOOK_ALADIN_API_ISBN_NOT_FOUND); } - JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY); + JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue()); - return subInfo.path(PAGE_COUNT_PARSING_KEY).asInt(); + return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt(); } catch (IOException e) { throw new BusinessException(BOOK_ALADIN_API_PARSING_ERROR); } From c248b1587234468d3a99b075b2fda124d22528f3 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 15:50:30 +0900 Subject: [PATCH 17/25] =?UTF-8?q?[refactor]=20todo=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/api/CompositeBookApiAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java index ddb95eb45..06b61ebfb 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java @@ -43,7 +43,7 @@ public Book loadBookWithPageByIsbn(String isbn) { detailBookByKeyword.title(), isbn, detailBookByKeyword.author(), - false, + false, // TODO : 추후 BestSeller 도입되면 고려해야함 detailBookByKeyword.publisher(), detailBookByKeyword.imageUrl(), pageCount, From fb3b5c2442a6ff030b05558cd63a220e48f82a87 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 16:44:04 +0900 Subject: [PATCH 18/25] =?UTF-8?q?[feat]=20Room=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20validation=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - withoutId 는 새로운 방을 처음 생성하는 것이므로, 방 퍼센티지를 직접적으로 0으로 설정하도록 코드 수정 --- .../service/RoomCreateService.java | 1 - .../java/konkuk/thip/room/domain/Room.java | 26 +++++++++++- .../konkuk/thip/room/domain/RoomTest.java | 42 ++++++++++++++++++- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java index 719081eac..a3dec5d47 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java @@ -38,7 +38,6 @@ public Long createRoom(RoomCreateCommand command) { command.description(), command.isPublic(), command.password(), - 0, command.progressStartDate(), command.progressEndDate(), command.recruitCount(), diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 1bd874b96..73dc29516 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -36,8 +36,9 @@ public class Room extends BaseDomainEntity { private Long categoryId; - public static Room withoutId(String title, String description, boolean isPublic, String password, double roomPercentage, LocalDate startDate, LocalDate endDate, int recruitCount, Long bookId, Long categoryId) { + public static Room withoutId(String title, String description, boolean isPublic, String password, LocalDate startDate, LocalDate endDate, int recruitCount, Long bookId, Long categoryId) { validateVisibilityPasswordRule(isPublic, password); + validateDates(startDate, endDate); return Room.builder() .id(null) @@ -45,7 +46,7 @@ public static Room withoutId(String title, String description, boolean isPublic, .description(description) .isPublic(isPublic) .password(password) - .roomPercentage(roomPercentage) + .roomPercentage(0) // 처음 Room 생성 시 -> 0% .startDate(startDate) .endDate(endDate) .recruitCount(recruitCount) @@ -67,6 +68,27 @@ private static void validateVisibilityPasswordRule(boolean isPublic, String pass } } + private static void validateDates(LocalDate startDate, LocalDate endDate) { + LocalDate today = LocalDate.now(); + if (!startDate.isBefore(endDate)) { + String message = String.format( + "시작일(%s)은 종료일(%s)보다 이전이어야 합니다.", + startDate, endDate + ); + throw new InvalidStateException(INVALID_ROOM_CREATE, + new IllegalArgumentException(message)); + } + + if (startDate.isBefore(today)) { + String message = String.format( + "시작일(%s)은 현재 날짜(%s) 이후여야 합니다.", // 현재 날짜 포함 + startDate, today + ); + throw new InvalidStateException(INVALID_ROOM_CREATE, + new IllegalArgumentException(message)); + } + } + public boolean isExpired() { return this.getStatus() == StatusType.EXPIRED; } diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java index 71f06fb6e..d5b434078 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -20,7 +20,7 @@ void withoutId_public_password_not_null() { InvalidStateException ex = assertThrows(InvalidStateException.class, () -> Room.withoutId( "제목", "설명", true, "1234", - 0.0, START, END, 5, 123L, 456L + START, END, 5, 123L, 456L ) ); assertInstanceOf(IllegalArgumentException.class, ex.getCause()); @@ -34,11 +34,49 @@ void withoutId_private_password_null() { InvalidStateException ex = assertThrows(InvalidStateException.class, () -> Room.withoutId( "제목", "설명", false, null, - 0.0, START, END, 5, 123L, 456L + START, END, 5, 123L, 456L ) ); assertInstanceOf(IllegalArgumentException.class, ex.getCause()); assertTrue(ex.getCause().getMessage().contains("방 공개/비공개 여부와 비밀번호 설정이 일치하지 않습니다.")); assertTrue(ex.getCause().getMessage().contains("공개 여부 = false, 비밀번호 존재 여부 = false")); } + + @Test + @DisplayName("withoutId: 시간 순서상 시작일 -> 종료일이 아닐 경우, InvalidStateException 발생한다.") + void withoutId_startDate_not_before_endDate() { + LocalDate start = LocalDate.of(2025, 8, 1); + LocalDate end = LocalDate.of(2025, 7, 1); + InvalidStateException ex = assertThrows(InvalidStateException.class, () -> + Room.withoutId( + "제목", "설명", false, "pass", + start, end, 5, 123L, 456L + ) + ); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains( + String.format("시작일(%s)은 종료일(%s)보다 이전이어야 합니다.", start, end) + )); + } + + @Test + @DisplayName("withoutId: 시작일이 현재 날짜보다 이전일 경우, InvalidStateException 발생한다.") + void withoutId_startDate_before_today() { + LocalDate today = LocalDate.now(); + LocalDate past = today.minusDays(1); + LocalDate future = today.plusDays(1); + + // 시작일이 현재시점 - 1 일 + InvalidStateException ex = assertThrows(InvalidStateException.class, () -> + Room.withoutId( + "제목", "설명", false, "pass", + past, future, 5, 123L, 456L + ) + ); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains( + String.format("시작일(%s)은 현재 날짜(%s) 이후여야 합니다.", past, today) + )); + } + } From bbacd4e45fe08929e9ac2afcacf8b7bd73580f57 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 16:51:33 +0900 Subject: [PATCH 19/25] =?UTF-8?q?[refactor]=20Book=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=82=B4=EB=B6=80?= =?UTF-8?q?=EC=97=90=20withId=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20void=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Port로부터 도메인 엔티티 전달받는 플로우로 Service 코드 수정 --- .../application/service/BookSavedService.java | 3 ++- .../java/konkuk/thip/book/domain/Book.java | 25 ++----------------- .../service/RoomCreateService.java | 7 ++++-- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 4872f1674..9b41d751c 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -56,7 +56,8 @@ public BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userI naverResult.description()); Long newBookId = bookCommandPort.save(newBook); - book = newBook.withId(newBookId); + + book = bookCommandPort.findById(newBookId); } // 유저가 저장한 책 목록 조회 diff --git a/src/main/java/konkuk/thip/book/domain/Book.java b/src/main/java/konkuk/thip/book/domain/Book.java index b80382540..7d5a2b785 100644 --- a/src/main/java/konkuk/thip/book/domain/Book.java +++ b/src/main/java/konkuk/thip/book/domain/Book.java @@ -55,32 +55,11 @@ public static Book withoutId(String title, String isbn, String authorName, boole .build(); } - public Book withId(Long id) { - return this.toBuilder() - .id(id) - .build(); - } - public boolean hasPageCount() { return pageCount != null && pageCount > 0; } - public Book changePageCount(Integer newPageCount) { - return this.toBuilder() - .pageCount(newPageCount) - .build(); - } - - private BookBuilder toBuilder() { - return Book.builder() - .id(id) - .title(title) - .isbn(isbn) - .authorName(authorName) - .bestSeller(bestSeller) - .publisher(publisher) - .imageUrl(imageUrl) - .pageCount(pageCount) - .description(description); + public void changePageCount(Integer newPageCount) { + this.pageCount = newPageCount; } } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java index a3dec5d47..2b19a097c 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java @@ -45,6 +45,9 @@ public Long createRoom(RoomCreateCommand command) { category.getId() ); + // TODO : 방 생성한 사람 (= api 호출 토큰에 포함된 userId) 이 해당 방에 속한 멤버라는 사실을 DB에 영속화 해야함 + // UserRoom 도메인이 정리되면 개발 ㄱㄱ + return roomCommandPort.save(room); } @@ -62,8 +65,8 @@ private Long resolveBookAndEnsurePage(String isbn) { private void updateBookPageCount(Book book) { Integer pageCount = bookApiQueryPort.findPageCountByIsbn(book.getIsbn()); - Book updated = book.changePageCount(pageCount); - bookCommandPort.updateForPageCount(updated); + book.changePageCount(pageCount); + bookCommandPort.updateForPageCount(book); } private Long saveNewBookWithPageCount(String isbn) { From 11dffd2cbaf935115c4237f5a946750c945331a0 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 17:26:21 +0900 Subject: [PATCH 20/25] =?UTF-8?q?[fix]=20test=20yml=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=AA=85=EC=8B=9C=20(#4?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/room/adapter/in/web/RoomCreateControllerTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java index d4da2db50..ed5ddf0e1 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java @@ -8,6 +8,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.util.HashMap; @@ -20,6 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest +@ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @DisplayName("방 생성 api controller 단위 테스트") class RoomCreateControllerTest { From b56f48cc324fe06be7292ee318113f1dd3bf1327 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 23:09:21 +0900 Subject: [PATCH 21/25] =?UTF-8?q?[feat]=20Room=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=95=B4?= =?UTF-8?q?=EC=8B=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room/adapter/out/mapper/RoomMapper.java | 4 ++-- .../java/konkuk/thip/room/domain/Room.java | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java index 9196e7f8d..ecca64cc9 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java +++ b/src/main/java/konkuk/thip/room/adapter/out/mapper/RoomMapper.java @@ -14,7 +14,7 @@ public RoomJpaEntity toJpaEntity(Room room, BookJpaEntity bookJpaEntity, Categor .title(room.getTitle()) .description(room.getDescription()) .isPublic(room.isPublic()) - .password(room.getPassword()) + .password(room.getHashedPassword()) .roomPercentage(room.getRoomPercentage()) .startDate(room.getStartDate()) .endDate(room.getEndDate()) @@ -30,7 +30,7 @@ public Room toDomainEntity(RoomJpaEntity roomJpaEntity) { .title(roomJpaEntity.getTitle()) .description(roomJpaEntity.getDescription()) .isPublic(roomJpaEntity.isPublic()) - .password(roomJpaEntity.getPassword()) + .hashedPassword(roomJpaEntity.getPassword()) .roomPercentage(roomJpaEntity.getRoomPercentage()) .startDate(roomJpaEntity.getStartDate()) .endDate(roomJpaEntity.getEndDate()) diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index 73dc29516..edb270d2a 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -5,6 +5,8 @@ import konkuk.thip.common.entity.StatusType; import lombok.Getter; import lombok.experimental.SuperBuilder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDate; @@ -14,6 +16,8 @@ @SuperBuilder public class Room extends BaseDomainEntity { + private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + private Long id; private String title; @@ -22,7 +26,7 @@ public class Room extends BaseDomainEntity { private boolean isPublic; - private String password; + private String hashedPassword; private double roomPercentage; @@ -40,12 +44,15 @@ public static Room withoutId(String title, String description, boolean isPublic, validateVisibilityPasswordRule(isPublic, password); validateDates(startDate, endDate); + // 비밀번호 해싱 + String hashedPassword = (password != null) ? PASSWORD_ENCODER.encode(password) : null; + return Room.builder() .id(null) .title(title) .description(description) .isPublic(isPublic) - .password(password) + .hashedPassword(hashedPassword) .roomPercentage(0) // 처음 Room 생성 시 -> 0% .startDate(startDate) .endDate(endDate) @@ -96,4 +103,11 @@ public boolean isExpired() { public void updateRoomPercentage(double roomPercentage) { this.roomPercentage = roomPercentage; } + + public boolean matchesPassword(String rawPassword) { + if (this.hashedPassword == null || rawPassword == null) { + return false; + } + return PASSWORD_ENCODER.matches(rawPassword, this.hashedPassword); + } } From acd59765d6d38558a316fe620f8482494920e9cc Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 23:20:24 +0900 Subject: [PATCH 22/25] =?UTF-8?q?[refactor]=20RoomTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비밀번호 매칭 메서드 테스트 코드 추가 - withoutId 메서드를 통해 비밀번호가 해싱되는 것을 보여주기 위한 테스트 코드 추가 - 기존 테스트 코드의 비밀번호를 4자리 숫자로 변경 --- .../konkuk/thip/room/domain/RoomTest.java | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java index d5b434078..6a70c823d 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -3,6 +3,8 @@ import konkuk.thip.common.exception.InvalidStateException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDate; @@ -11,8 +13,11 @@ @DisplayName("Room 단위 테스트") class RoomTest { - private final LocalDate START = LocalDate.of(2025, 7, 1); - private final LocalDate END = LocalDate.of(2025, 8, 1); + private static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + + private final LocalDate today = LocalDate.now(); + private final LocalDate START = today.plusDays(1); + private final LocalDate END = today.plusDays(32); @Test @DisplayName("withoutId: 공개 방이면서 password가 not null 이면, InvalidStateException 발생한다.") @@ -49,7 +54,7 @@ void withoutId_startDate_not_before_endDate() { LocalDate end = LocalDate.of(2025, 7, 1); InvalidStateException ex = assertThrows(InvalidStateException.class, () -> Room.withoutId( - "제목", "설명", false, "pass", + "제목", "설명", false, "1234", start, end, 5, 123L, 456L ) ); @@ -69,7 +74,7 @@ void withoutId_startDate_before_today() { // 시작일이 현재시점 - 1 일 InvalidStateException ex = assertThrows(InvalidStateException.class, () -> Room.withoutId( - "제목", "설명", false, "pass", + "제목", "설명", false, "1234", past, future, 5, 123L, 456L ) ); @@ -79,4 +84,53 @@ void withoutId_startDate_before_today() { )); } + @Test + @DisplayName("withoutId: 전달받은 비밀번호를 해싱해서 보관한다.") + void withoutId_password_hashing() { + String rawPassword = "1234"; + + Room room = Room.withoutId( + "제목", "설명", false, rawPassword, + START, END, 5, 123L, 456L + ); + + String hashed = room.getHashedPassword(); + + // 해시된 비밀번호가 null이 아니고 원문과 다름 + assertNotNull(hashed); + assertNotEquals(rawPassword, hashed); + + // matchesPassword를 통해 원문 검증 시 true + assertTrue(PASSWORD_ENCODER.matches(rawPassword, hashed)); + } + + @Test + @DisplayName("matchesPassword: 올바른 비밀번호면 true 반환") + void matchesPassword_correct_password() { + Room room = Room.withoutId( + "제목", "설명", false, "1234", + START, END, 5, 123L, 456L + ); + assertTrue(room.matchesPassword("1234")); + } + + @Test + @DisplayName("matchesPassword: 잘못된 비밀번호면 false 반환") + void matchesPassword_incorrect_password() { + Room room = Room.withoutId( + "제목", "설명", false, "1234", + START, END, 5, 123L, 456L + ); + assertFalse(room.matchesPassword("0000")); + } + + @Test + @DisplayName("matchesPassword: Room이 비밀번호가 설정되어 있지 않은 공개방일 경우, false 반환") + void matchesPassword_no_password() { + Room room = Room.withoutId( + "제목", "설명", true, null, + START, END, 5, 123L, 456L + ); + assertFalse(room.matchesPassword("0000")); + } } From 0ea01790d60424b7f921360c9d2f76e6a59910f5 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 8 Jul 2025 23:42:18 +0900 Subject: [PATCH 23/25] =?UTF-8?q?[refactor]=20=EC=88=98=EC=A0=95=EB=90=9C?= =?UTF-8?q?=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20Room=20=EC=83=9D=EC=84=B1=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'Room 활동 시작일은 Room을 생성하려는 시각보다 최소 1일 이후여야 한다' 라는 요구사항 반영 --- .../java/konkuk/thip/room/domain/Room.java | 5 +++-- .../konkuk/thip/room/domain/RoomTest.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index edb270d2a..d630ce4b4 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -77,6 +77,7 @@ private static void validateVisibilityPasswordRule(boolean isPublic, String pass private static void validateDates(LocalDate startDate, LocalDate endDate) { LocalDate today = LocalDate.now(); + if (!startDate.isBefore(endDate)) { String message = String.format( "시작일(%s)은 종료일(%s)보다 이전이어야 합니다.", @@ -86,9 +87,9 @@ private static void validateDates(LocalDate startDate, LocalDate endDate) { new IllegalArgumentException(message)); } - if (startDate.isBefore(today)) { + if (!startDate.isAfter(today)) { String message = String.format( - "시작일(%s)은 현재 날짜(%s) 이후여야 합니다.", // 현재 날짜 포함 + "시작일(%s)은 현재 날짜(%s) 이후여야 합니다.", // 현재 날짜 미포함 startDate, today ); throw new InvalidStateException(INVALID_ROOM_CREATE, diff --git a/src/test/java/konkuk/thip/room/domain/RoomTest.java b/src/test/java/konkuk/thip/room/domain/RoomTest.java index 6a70c823d..fd11dd10e 100644 --- a/src/test/java/konkuk/thip/room/domain/RoomTest.java +++ b/src/test/java/konkuk/thip/room/domain/RoomTest.java @@ -84,6 +84,25 @@ void withoutId_startDate_before_today() { )); } + @Test + @DisplayName("withoutId: 시작일 현재 날짜와 동일할 경우, InvalidStateException 발생한다.") + void withoutId_startDate_is_today() { + LocalDate today = LocalDate.now(); + LocalDate future = today.plusDays(1); + + // 시작일 == 현재시점 + InvalidStateException ex = assertThrows(InvalidStateException.class, () -> + Room.withoutId( + "제목", "설명", false, "1234", + today, future, 5, 123L, 456L + ) + ); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains( + String.format("시작일(%s)은 현재 날짜(%s) 이후여야 합니다.", today, today) + )); + } + @Test @DisplayName("withoutId: 전달받은 비밀번호를 해싱해서 보관한다.") void withoutId_password_hashing() { From 9539c14d5c11178dc8e18704a6ad76ad5926b066 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 9 Jul 2025 00:14:31 +0900 Subject: [PATCH 24/25] =?UTF-8?q?[refactor]=20=EB=AA=A8=EC=9E=84=EB=B0=A9?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=9D=B8=EC=9B=90=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - request dto 내부에서 모집 인원과 관련된 bean validation 추가 - 관련해서 controller 단위 테스트 코드 추가 --- .../room/adapter/in/web/request/RoomCreateRequest.java | 1 + .../room/adapter/in/web/RoomCreateControllerTest.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java index 6b8759472..fb025c30f 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/request/RoomCreateRequest.java @@ -33,6 +33,7 @@ public record RoomCreateRequest( String progressEndDate, @Min(value = 1, message = "모집 인원은 최소 1명이어야 합니다.") + @Max(value = 30, message = "모집 인원은 최대 30명이어야 합니다.") int recruitCount, @Nullable diff --git a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java index ed5ddf0e1..cad63a039 100644 --- a/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java +++ b/src/test/java/konkuk/thip/room/adapter/in/web/RoomCreateControllerTest.java @@ -153,6 +153,14 @@ void less_than_one() throws Exception { req.put("recruitCount", 0); assertBad(req, "모집 인원은 최소 1명이어야 합니다."); } + + @Test + @DisplayName("30 초과일 때 400 error") + void greater_than_max() throws Exception { + Map req = buildValidRequest(); + req.put("recruitCount", 31); + assertBad(req, "모집 인원은 최대 30명이어야 합니다."); + } } @Nested From e13e5e631c89de4f149dbb677e97a2cb7d08ff4f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 9 Jul 2025 00:26:32 +0900 Subject: [PATCH 25/25] =?UTF-8?q?[refactor]=20todo=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20userId=20Service=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EC=9D=B8=EC=9E=90=EB=A1=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/room/adapter/in/web/RoomCommandController.java | 5 +++-- .../thip/room/application/port/in/RoomCreateUseCase.java | 2 +- .../thip/room/application/service/RoomCreateService.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java index dc09f4b5c..acfd5076f 100644 --- a/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java +++ b/src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.room.adapter.in.web.request.RoomCreateRequest; import konkuk.thip.room.adapter.in.web.response.RoomCreateResponse; import konkuk.thip.room.application.port.in.RoomCreateUseCase; @@ -17,9 +18,9 @@ public class RoomCommandController { private final RoomCreateUseCase roomCreateUseCase; @PostMapping("/rooms") - public BaseResponse createRoom(@Valid @RequestBody RoomCreateRequest request) { + public BaseResponse createRoom(@Valid @RequestBody RoomCreateRequest request, @UserId Long userId) { return BaseResponse.ok(RoomCreateResponse.of( - roomCreateUseCase.createRoom(request.toCommand()) + roomCreateUseCase.createRoom(request.toCommand(), userId) )); } } diff --git a/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java b/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java index 0adb9306e..b53e36b28 100644 --- a/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java +++ b/src/main/java/konkuk/thip/room/application/port/in/RoomCreateUseCase.java @@ -4,5 +4,5 @@ public interface RoomCreateUseCase { - Long createRoom(RoomCreateCommand command); + Long createRoom(RoomCreateCommand command, Long userId); } diff --git a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java index 2b19a097c..dfd5b68a1 100644 --- a/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java +++ b/src/main/java/konkuk/thip/room/application/service/RoomCreateService.java @@ -25,7 +25,7 @@ public class RoomCreateService implements RoomCreateUseCase { @Override @Transactional - public Long createRoom(RoomCreateCommand command) { + public Long createRoom(RoomCreateCommand command, Long userId) { // 1. Category 찾기 Category category = categoryCommandPort.findByValue(command.category());