From e5507aca8177780e27ee63c504b198f246a5fcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EB=B9=88?= Date: Mon, 19 Jan 2026 23:19:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=8B=A8=EC=B2=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 분실물 게시글 작성자의 단체 정보를 관리하기 위한 organizations 테이블 및 엔티티 추가 --- .../organization/model/Organization.java | 40 +++++++++++++++++++ .../repository/OrganizationRepository.java | 12 ++++++ .../V228__add_organizations_table.sql | 16 ++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/organization/model/Organization.java create mode 100644 src/main/java/in/koreatech/koin/domain/organization/repository/OrganizationRepository.java create mode 100644 src/main/resources/db/migration/V228__add_organizations_table.sql diff --git a/src/main/java/in/koreatech/koin/domain/organization/model/Organization.java b/src/main/java/in/koreatech/koin/domain/organization/model/Organization.java new file mode 100644 index 000000000..64f306298 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/organization/model/Organization.java @@ -0,0 +1,40 @@ +package in.koreatech.koin.domain.organization.model; + +import in.koreatech.koin.common.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "organizations") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Organization extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @NotNull + @Column(name = "user_id", nullable = false, unique = true) + private Integer userId; + + @NotNull + @Column(name = "name", nullable = false, length = 100) + private String name; + + @NotNull + @Column(name = "location", nullable = false, length = 255) + private String location; + + @NotNull + @Column(name = "is_deleted", nullable = false) + private Boolean isDeleted = false; +} diff --git a/src/main/java/in/koreatech/koin/domain/organization/repository/OrganizationRepository.java b/src/main/java/in/koreatech/koin/domain/organization/repository/OrganizationRepository.java new file mode 100644 index 000000000..c750179fe --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/organization/repository/OrganizationRepository.java @@ -0,0 +1,12 @@ +package in.koreatech.koin.domain.organization.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.domain.organization.model.Organization; + +public interface OrganizationRepository extends Repository { + + Optional findByUserIdAndIsDeletedFalse(Integer userId); +} diff --git a/src/main/resources/db/migration/V228__add_organizations_table.sql b/src/main/resources/db/migration/V228__add_organizations_table.sql new file mode 100644 index 000000000..0d6e0995f --- /dev/null +++ b/src/main/resources/db/migration/V228__add_organizations_table.sql @@ -0,0 +1,16 @@ +-- organizations 테이블 생성 (단체 정보 관리) +CREATE TABLE `organizations` ( + `id` INT NOT NULL AUTO_INCREMENT, + `user_id` INT NOT NULL COMMENT '단체 계정 user_id', + `name` VARCHAR(100) NOT NULL COMMENT '단체명 (예: 총학생회, 컴퓨터공학부)', + `location` VARCHAR(255) NOT NULL COMMENT '방문 장소 (예: 학생회관 320호 총학생회 사무실)', + `is_deleted` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_organizations_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='단체 정보'; + +-- 초기 데이터 (총학생회) - user_id는 실제 총학생회 계정 ID로 변경 필요 +-- INSERT INTO `organizations` (`user_id`, `name`, `location`) +-- VALUES ({총학생회_user_id}, '총학생회', '학생회관 320호 총학생회 사무실로 방문'); From c5802f070ea12d63ca5db55782135989be197a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EB=B9=88?= Date: Mon, 19 Jan 2026 23:20:43 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=B6=84=EC=8B=A4=EB=AC=BC=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20V2=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * is_council 대신 organization 객체로 단체 정보 제공 --- .../controller/LostItemArticleApi.java | 19 +++ .../controller/LostItemArticleController.java | 9 ++ .../dto/LostItemArticleResponseV2.java | 127 ++++++++++++++++++ .../service/LostItemArticleService.java | 23 ++++ 4 files changed, 178 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleResponseV2.java diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java index c17bfa4a8..c5d70ed74 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleApi.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponseV2; import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; import in.koreatech.koin.domain.community.article.dto.LostItemArticleUpdateRequest; import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; @@ -110,6 +111,24 @@ ResponseEntity getLostItemArticle( @UserId Integer userId ); + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "분실물 게시글 단건 조회 V2", description = """ + ### V2 변경점 + - `is_council` 필드 제거 + - `organization` 객체 추가 (단체 정보) + - 일반 유저 게시글인 경우 `organization: null` + """) + @GetMapping("/lost-item/v2/{id}") + ResponseEntity getLostItemArticleV2( + @Parameter(in = PATH) @PathVariable("id") Integer articleId, + @UserId Integer userId + ); + @ApiResponses( value = { @ApiResponse(responseCode = "201"), diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java index 2bc7e44cf..12bd5c0fb 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/LostItemArticleController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponseV2; import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; import in.koreatech.koin.domain.community.article.dto.LostItemArticleUpdateRequest; import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; @@ -87,6 +88,14 @@ public ResponseEntity getLostItemArticle( return ResponseEntity.ok().body(lostItemArticleService.getLostItemArticle(articleId, userId)); } + @GetMapping("/lost-item/v2/{id}") + public ResponseEntity getLostItemArticleV2( + @PathVariable("id") Integer articleId, + @UserId Integer userId + ) { + return ResponseEntity.ok().body(lostItemArticleService.getLostItemArticleV2(articleId, userId)); + } + @PostMapping("/lost-item") public ResponseEntity createLostItemArticle( @Auth(permit = {GENERAL, STUDENT, COUNCIL}) Integer studentId, diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleResponseV2.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleResponseV2.java new file mode 100644 index 000000000..2e1b7f6bc --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/LostItemArticleResponseV2.java @@ -0,0 +1,127 @@ +package in.koreatech.koin.domain.community.article.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.article.model.LostItemArticle; +import in.koreatech.koin.domain.community.article.model.LostItemImage; +import in.koreatech.koin.domain.organization.model.Organization; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record LostItemArticleResponseV2( + @Schema(description = "게시글 id", example = "17368", requiredMode = REQUIRED) + Integer id, + + @Schema(description = "게시판 id", example = "14", requiredMode = REQUIRED) + Integer boardId, + + @Schema(description = "게시글 타입", example = "LOST", requiredMode = NOT_REQUIRED) + String type, + + @Schema(description = "분실물 종류", example = "신분증", requiredMode = REQUIRED) + String category, + + @Schema(description = "습득 장소", example = "학생회관 앞", requiredMode = REQUIRED) + String foundPlace, + + @Schema(description = "습득 날짜", example = "2025-01-01", requiredMode = REQUIRED) + LocalDate foundDate, + + @Schema(description = "본문", example = "학생회관 앞 계단에 …") + String content, + + @Schema(description = "작성자", example = "총학생회", requiredMode = REQUIRED) + String author, + + @Schema(description = "단체 정보 (일반 유저인 경우 null)", requiredMode = NOT_REQUIRED) + InnerOrganizationResponse organization, + + @Schema(description = "내 게시글 여부", example = "true", requiredMode = NOT_REQUIRED) + Boolean isMine, + + @Schema(description = "분실물 게시글 찾음 상태 여부", example = "false", requiredMode = REQUIRED) + Boolean isFound, + + @Schema(description = "분실물 사진") + List images, + + @Schema(description = "이전글 id", example = "17367") + Integer prevId, + + @Schema(description = "다음글 id", example = "17369") + Integer nextId, + + @Schema(description = "등록일", example = "2025-01-10", requiredMode = REQUIRED) + LocalDate registeredAt, + + @Schema(description = "수정일", example = "2025-01-10 16:53:22", requiredMode = REQUIRED) + LocalDateTime updatedAt +) { + + public static LostItemArticleResponseV2 of(Article article, Boolean isMine, Organization organization) { + LostItemArticle lostItemArticle = article.getLostItemArticle(); + + return new LostItemArticleResponseV2( + article.getId(), + article.getBoard().getId(), + lostItemArticle.getType(), + lostItemArticle.getCategory(), + lostItemArticle.getFoundPlace(), + lostItemArticle.getFoundDate(), + article.getContent(), + article.getAuthor(), + organization != null ? InnerOrganizationResponse.from(organization) : null, + isMine, + lostItemArticle.getIsFound(), + lostItemArticle.getImages().stream() + .filter(image -> !image.getIsDeleted()) + .map(InnerLostItemImageResponse::from) + .toList(), + article.getPrevId(), + article.getNextId(), + article.getRegisteredAt(), + article.getUpdatedAt() + ); + } + + @JsonNaming(value = SnakeCaseStrategy.class) + public record InnerOrganizationResponse( + @Schema(description = "단체명", example = "총학생회", requiredMode = REQUIRED) + String name, + + @Schema(description = "방문 장소", example = "학생회관 320호 총학생회 사무실로 방문", requiredMode = REQUIRED) + String location + ) { + public static InnerOrganizationResponse from(Organization organization) { + return new InnerOrganizationResponse( + organization.getName(), + organization.getLocation() + ); + } + } + + @JsonNaming(value = SnakeCaseStrategy.class) + public record InnerLostItemImageResponse( + @Schema(description = "분실물 이미지 id", example = "1", requiredMode = REQUIRED) + Integer id, + + @Schema(description = "이미지 Url", example = "https://api.koreatech.in/image.jpg", requiredMode = REQUIRED) + String imageUrl + ) { + public static InnerLostItemImageResponse from(LostItemImage lostItemImage) { + return new InnerLostItemImageResponse( + lostItemImage.getId(), + lostItemImage.getImageUrl() + ); + } + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java index 724df8b4c..06a0cca28 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/LostItemArticleService.java @@ -16,6 +16,7 @@ import in.koreatech.koin.common.event.ArticleKeywordEvent; import in.koreatech.koin.common.model.Criteria; import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponse; +import in.koreatech.koin.domain.community.article.dto.LostItemArticleResponseV2; import in.koreatech.koin.domain.community.article.dto.LostItemArticleStatisticsResponse; import in.koreatech.koin.domain.community.article.dto.LostItemArticleUpdateRequest; import in.koreatech.koin.domain.community.article.dto.LostItemArticlesRequest; @@ -33,6 +34,8 @@ import in.koreatech.koin.domain.community.article.repository.BoardRepository; import in.koreatech.koin.domain.community.article.repository.LostItemArticleRepository; import in.koreatech.koin.domain.community.util.KeywordExtractor; +import in.koreatech.koin.domain.organization.model.Organization; +import in.koreatech.koin.domain.organization.repository.OrganizationRepository; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.global.auth.exception.AuthorizationException; @@ -58,6 +61,7 @@ public class LostItemArticleService { private final LostItemArticleRepository lostItemArticleRepository; private final BoardRepository boardRepository; private final UserRepository userRepository; + private final OrganizationRepository organizationRepository; private final PopularKeywordTracker popularKeywordTracker; private final ApplicationEventPublisher eventPublisher; private final KeywordExtractor keywordExtractor; @@ -141,6 +145,25 @@ public LostItemArticleResponse getLostItemArticle(Integer articleId, Integer use return LostItemArticleResponse.of(article, isMine); } + public LostItemArticleResponseV2 getLostItemArticleV2(Integer articleId, Integer userId) { + Article article = articleRepository.getById(articleId); + setPrevNextArticle(LOST_ITEM_BOARD_ID, article); + + LostItemArticle lostItemArticle = article.getLostItemArticle(); + User author = lostItemArticle.getAuthor(); + + boolean isMine = author != null && Objects.equals(author.getId(), userId); + + Organization organization = null; + if (author != null) { + organization = organizationRepository + .findByUserIdAndIsDeletedFalse(author.getId()) + .orElse(null); + } + + return LostItemArticleResponseV2.of(article, isMine, organization); + } + @Transactional public LostItemArticleResponse createLostItemArticle(Integer userId, LostItemArticlesRequest requests) { Board lostItemBoard = boardRepository.getById(LOST_ITEM_BOARD_ID);