Repository: DguFarmSystem/HomePage-BE
Pull request #117: [feat] 프로젝트
Author: login: saokiritoni
Labels: name: ✨ feature
description: 새로운 기능
Created: 2025-03-22T09:19:51Z | Merged: 2025-03-26T08:57:25Z

## 🌱 관련 이슈
- close : #42 
 
## 🌱 작업 사항
### 홈페이지용
- 프로젝트 기수, 트랙 필터링 조회
- 프로젝트 단일조회

### 파밍로그용
- 프로젝트 등록 신청
- 신청한 프로젝트 목록 조회

### 관리자용
- 프로젝트 승인
- 프로젝트 거절
- 승인 대기중인 프로젝트 목록 조회

## 🌱 참고 사항
- security에 api/home/** 추가 필요
- 블로그도 홈페이지용은 home으로 수정 필요
- 블로그도 페이징 통일 필요


Diff:
diff --git a/src/main/java/org/farmsystem/homepage/domain/common/entity/Track.java b/src/main/java/org/farmsystem/homepage/domain/common/entity/Track.java
index 84b9d595..059d7c18 100644
--- a/src/main/java/org/farmsystem/homepage/domain/common/entity/Track.java
+++ b/src/main/java/org/farmsystem/homepage/domain/common/entity/Track.java
@@ -1,5 +1,14 @@
 package org.farmsystem.homepage.domain.common.entity;
 
+import java.util.Arrays;
+
 public enum Track {
-    UNION, GAMING_VIDEO, SECURITY_WEB, AI, IOT_ROBOTICS, BIGDATA
+    UNION, GAMING_VIDEO, SECURITY_WEB, AI, IOT_ROBOTICS, BIGDATA;
+
+    public static Track from(String value) {
+        return Arrays.stream(values())
+                .filter(t -> t.name().equalsIgnoreCase(value))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 트랙: " + value));
+    }
 }
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java
new file mode 100644
index 00000000..8243e0b4
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectApi.java
@@ -0,0 +1,86 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectApprovalResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Tag(name = "[관리자] 프로젝트 승인/거절 API", description = "관리자가 프로젝트를 승인/거절/조회합니다.")
+@RequestMapping("/api/admin/projects")
+public interface AdminProjectApi {
+
+    @Operation(
+            summary = "프로젝트 승인",
+            description = "관리자가 프로젝트를 승인합니다.",
+            security = @SecurityRequirement(name = "token")
+    )
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "승인 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = ProjectApprovalResponseDTO.class)
+                    )
+            ),
+            @ApiResponse(responseCode = "404", description = "존재하지 않는 프로젝트 ID")
+    })
+    @PatchMapping("/{projectId}/approve")
+    ResponseEntity<SuccessResponse<?>> approveProject(
+            @Parameter(hidden = true) Long adminUserId,
+            @PathVariable Long projectId
+    );
+
+    @Operation(
+            summary = "프로젝트 거절",
+            description = "관리자가 프로젝트를 거절합니다.",
+            security = @SecurityRequirement(name = "token")
+    )
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "거절 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = ProjectApprovalResponseDTO.class)
+                    )
+            ),
+            @ApiResponse(responseCode = "404", description = "존재하지 않는 프로젝트 ID")
+    })
+    @PatchMapping("/{projectId}/reject")
+    ResponseEntity<SuccessResponse<?>> rejectProject(
+            @Parameter(hidden = true) Long adminUserId,
+            @PathVariable Long projectId
+    );
+
+    @Operation(
+            summary = "승인 대기 중인 프로젝트 목록",
+            description = "관리자가 아직 승인되지 않은 프로젝트들을 조회합니다.",
+            security = @SecurityRequirement(name = "token")
+    )
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "조회 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            array = @ArraySchema(schema = @Schema(implementation = ProjectResponseDTO.class))
+                    )
+            )
+    })
+    @GetMapping("/pending")
+    ResponseEntity<SuccessResponse<?>> getPendingProjects();
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectController.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectController.java
new file mode 100644
index 00000000..8f59f033
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/AdminProjectController.java
@@ -0,0 +1,37 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.project.service.AdminProjectService;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/admin/projects")
+@RequiredArgsConstructor
+public class AdminProjectController implements AdminProjectApi{
+
+    private final AdminProjectService adminProjectService;
+
+    @PatchMapping("/{projectId}/approve")
+    public ResponseEntity<SuccessResponse<?>> approveProject(
+            @PathVariable Long projectId,
+            @AuthenticationPrincipal Long adminUserId
+    ) {
+        return SuccessResponse.ok(adminProjectService.approveProject(projectId, adminUserId));
+    }
+
+    @PatchMapping("/{projectId}/reject")
+    public ResponseEntity<SuccessResponse<?>> rejectProject(
+            @PathVariable Long projectId,
+            @AuthenticationPrincipal Long adminUserId
+    ) {
+        return SuccessResponse.ok(adminProjectService.rejectProject(projectId, adminUserId));
+    }
+
+    @GetMapping("/pending")
+    public ResponseEntity<SuccessResponse<?>> getPendingProjects() {
+        return SuccessResponse.ok(adminProjectService.getPendingProjects());
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectApi.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectApi.java
new file mode 100644
index 00000000..044fa8f9
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectApi.java
@@ -0,0 +1,55 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectPageResultDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@Tag(name = "[홍보 페이지] 프로젝트 조회 API", description = "기수 및 트랙 필터링, 단일 프로젝트 상세 조회")
+@RequestMapping("/api/home/projects")
+public interface HomeProjectApi {
+
+    @Operation(summary = "필터링된 프로젝트 조회", description = "기수 및 트랙으로 필터링된 프로젝트를 페이징 조회합니다.")
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "조회 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = ProjectPageResultDTO.class)
+                    )
+            )
+    })
+    @GetMapping("/filter")
+    ResponseEntity<SuccessResponse<?>> getFilteredProjects(
+            @RequestParam(required = false) Integer generation,
+            @RequestParam(required = false) String track,
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "10") int size
+    );
+
+    @Operation(summary = "단일 프로젝트 상세 조회", description = "projectId로 프로젝트 상세 정보를 조회합니다.")
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "조회 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            schema = @Schema(implementation = ProjectResponseDTO.class)
+                    )
+            ),
+            @ApiResponse(responseCode = "404", description = "존재하지 않는 프로젝트 ID")
+    })
+    @GetMapping("/{projectId}")
+    ResponseEntity<SuccessResponse<?>> getProjectDetail(@PathVariable Long projectId);
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectController.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectController.java
new file mode 100644
index 00000000..ef503965
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/HomeProjectController.java
@@ -0,0 +1,38 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectPageResultDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectSimpleResponseDTO;
+import org.farmsystem.homepage.domain.project.service.ProjectService;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/home/projects")
+@RequiredArgsConstructor
+public class HomeProjectController implements HomeProjectApi{
+
+    private final ProjectService projectService;
+
+    @GetMapping("/filter")
+    public ResponseEntity<SuccessResponse<?>> getFilteredProjects(
+            @RequestParam(required = false) Integer generation,
+            @RequestParam(required = false) String track,
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "10") int size
+    ) {
+        Track trackEnum = (track != null) ? Track.from(track) : null;
+        Page<ProjectSimpleResponseDTO> projectPage = projectService.getFilteredProjects(generation, trackEnum, page, size);
+        return SuccessResponse.ok(ProjectPageResultDTO.of(projectPage, PageRequest.of(page, size)));
+    }
+
+    @GetMapping("/{projectId}")
+    public ResponseEntity<SuccessResponse<?>> getProjectDetail(@PathVariable Long projectId) {
+        return SuccessResponse.ok(projectService.getProjectDetail(projectId));
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java
new file mode 100644
index 00000000..a97fed2f
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectApi.java
@@ -0,0 +1,64 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import org.farmsystem.homepage.domain.project.dto.request.ProjectRequestDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Tag(name = "[파밍로그] 프로젝트 신청 API", description = "프로젝트 신청 및 조회")
+@RequestMapping("/api/projects")
+public interface ProjectApi {
+
+    @Operation(
+            summary = "프로젝트 신청",
+            description = "유저가 프로젝트를 신청합니다.",
+            security = @SecurityRequirement(name = "token")
+    )
+    @ApiResponses({
+            @ApiResponse(responseCode = "204", description = "신청 성공")
+    })
+    @PostMapping
+    ResponseEntity<SuccessResponse<?>> applyForProject(
+            @io.swagger.v3.oas.annotations.Parameter(hidden = true) Long userId,
+            @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    description = "신청할 프로젝트 정보",
+                    required = true,
+                    content = @Content(schema = @Schema(implementation = ProjectRequestDTO.class))
+            )
+            @Valid @RequestBody ProjectRequestDTO request
+    );
+
+
+    @Operation(
+            summary = "내가 신청한 프로젝트 목록",
+            description = "유저가 신청한 프로젝트 목록을 조회합니다.",
+            security = @SecurityRequirement(name = "token")
+    )
+    @ApiResponses({
+            @ApiResponse(
+                    responseCode = "200",
+                    description = "조회 성공",
+                    content = @Content(
+                            mediaType = "application/json",
+                            array = @ArraySchema(schema = @Schema(implementation = ProjectResponseDTO.class))
+                    )
+            )
+    })
+    @GetMapping("/my")
+    ResponseEntity<SuccessResponse<?>> getMyProjects(
+            @io.swagger.v3.oas.annotations.Parameter(hidden = true) Long userId
+    );
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectController.java b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectController.java
new file mode 100644
index 00000000..1c9643ff
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/controller/ProjectController.java
@@ -0,0 +1,34 @@
+package org.farmsystem.homepage.domain.project.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.project.dto.request.ProjectRequestDTO;
+import org.farmsystem.homepage.domain.project.service.ProjectService;
+import org.farmsystem.homepage.global.common.SuccessResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+
+@RestController
+@RequestMapping("/api/projects")
+@RequiredArgsConstructor
+public class ProjectController implements ProjectApi{
+
+    private final ProjectService projectService;
+
+    @PostMapping
+    public ResponseEntity<SuccessResponse<?>> applyForProject(
+            @AuthenticationPrincipal Long userId,
+            @RequestBody ProjectRequestDTO request
+    ) {
+        projectService.applyForProject(userId, request);
+        return SuccessResponse.noContent();
+    }
+
+    @GetMapping("/my")
+    public ResponseEntity<SuccessResponse<?>> getMyProjects(@AuthenticationPrincipal Long userId) {
+        return SuccessResponse.ok(projectService.getMyProjects(userId));
+    }
+}
+
+
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/dto/request/ProjectRequestDTO.java b/src/main/java/org/farmsystem/homepage/domain/project/dto/request/ProjectRequestDTO.java
new file mode 100644
index 00000000..45f0d6a5
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/dto/request/ProjectRequestDTO.java
@@ -0,0 +1,15 @@
+package org.farmsystem.homepage.domain.project.dto.request;
+
+import java.util.List;
+
+public record ProjectRequestDTO(
+        String title,
+        String introduction,
+        String content,
+        String thumbnailImageUrl,
+        String bodyImageUrl,
+        String githubLink,
+        String deploymentLink,
+        String resourceLink,
+        List<String> participants
+) {}
\ No newline at end of file
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectApprovalResponseDTO.java b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectApprovalResponseDTO.java
new file mode 100644
index 00000000..554d9059
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectApprovalResponseDTO.java
@@ -0,0 +1,11 @@
+package org.farmsystem.homepage.domain.project.dto.response;
+
+import java.time.LocalDateTime;
+
+public record ProjectApprovalResponseDTO(
+        Long projectId,
+        String title,
+        String approvalStatus,
+        String approvedBy,
+        LocalDateTime approvedAt
+) {}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectPageResultDTO.java b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectPageResultDTO.java
new file mode 100644
index 00000000..c04d090e
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectPageResultDTO.java
@@ -0,0 +1,19 @@
+package org.farmsystem.homepage.domain.project.dto.response;
+
+import org.farmsystem.homepage.domain.common.dto.response.PageResponseDTO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+
+public record ProjectPageResultDTO<T>(
+        List<T> content,
+        PageResponseDTO pageInfo
+) {
+    public static <T> ProjectPageResultDTO<T> of(Page<T> page, Pageable pageable) {
+        return new ProjectPageResultDTO<>(
+                page.getContent(),
+                PageResponseDTO.of(page, pageable)
+        );
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectResponseDTO.java b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectResponseDTO.java
new file mode 100644
index 00000000..5fb2f489
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectResponseDTO.java
@@ -0,0 +1,38 @@
+package org.farmsystem.homepage.domain.project.dto.response;
+
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.entity.Project;
+
+import java.util.List;
+
+public record ProjectResponseDTO(
+        Long projectId,
+        String title,
+        String introduction,
+        String content,
+        String thumbnailImageUrl,
+        String bodyImageUrl,
+        String githubLink,
+        String deploymentLink,
+        String resourceLink,
+        List<String> participants,
+        String approvalStatus,
+        Track track
+) {
+    public static ProjectResponseDTO fromEntity(Project project) {
+        return new ProjectResponseDTO(
+                project.getProjectId(),
+                project.getTitle(),
+                project.getIntroduction(),
+                project.getContent(),
+                project.getThumbnailImageUrl(),
+                project.getBodyImageUrl(),
+                project.getGithubLink(),
+                project.getDeploymentLink(),
+                project.getResourceLink(),
+                project.getParticipants(),
+                project.getApprovalStatus().name(),
+                project.getTrack()
+        );
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectSimpleResponseDTO.java b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectSimpleResponseDTO.java
new file mode 100644
index 00000000..d33a18d6
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/dto/response/ProjectSimpleResponseDTO.java
@@ -0,0 +1,24 @@
+package org.farmsystem.homepage.domain.project.dto.response;
+
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.entity.Project;
+
+public record ProjectSimpleResponseDTO(
+        Long projectId,
+        String title,
+        String introduction,
+        String thumbnailImageUrl,
+        Track track,
+        int generation
+) {
+    public static ProjectSimpleResponseDTO fromEntity(Project project) {
+        return new ProjectSimpleResponseDTO(
+                project.getProjectId(),
+                project.getTitle(),
+                project.getIntroduction(),
+                project.getThumbnailImageUrl(),
+                project.getTrack(),
+                project.getGeneration()
+        );
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/entity/ApprovalStatus.java b/src/main/java/org/farmsystem/homepage/domain/project/entity/ApprovalStatus.java
new file mode 100644
index 00000000..a909dfac
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/entity/ApprovalStatus.java
@@ -0,0 +1,7 @@
+package org.farmsystem.homepage.domain.project.entity;
+
+public enum ApprovalStatus {
+    PENDING,
+    APPROVED,
+    REJECTED
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/entity/Project.java b/src/main/java/org/farmsystem/homepage/domain/project/entity/Project.java
new file mode 100644
index 00000000..423409c0
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/entity/Project.java
@@ -0,0 +1,87 @@
+package org.farmsystem.homepage.domain.project.entity;
+
+import jakarta.persistence.*;
+import lombok.*;
+import org.farmsystem.homepage.domain.common.entity.BaseTimeEntity;
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.user.entity.User;
+import org.farmsystem.homepage.global.error.ErrorCode;
+import org.farmsystem.homepage.global.error.exception.BusinessException;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity
+@Table(name = "project")
+public class Project extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long projectId;
+
+    @Column(nullable = false)
+    private String title;
+
+    @Column(nullable = false)
+    private String introduction;
+
+    @Column(nullable = false)
+    private String content;
+
+    private String thumbnailImageUrl;
+    private String bodyImageUrl;
+    private String githubLink;
+    private String deploymentLink;
+    private String resourceLink;
+
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "user_id", nullable = false)
+    private User user;
+
+    @Column(nullable = false)
+    private Integer generation;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    private Track track;
+
+    @ElementCollection
+    @CollectionTable(name = "project_participants", joinColumns = @JoinColumn(name = "project_id"))
+    @Column(name = "participant_info")
+    private List<String> participants;
+
+    @Enumerated(EnumType.STRING)
+    @Column(nullable = false)
+    private ApprovalStatus approvalStatus = ApprovalStatus.PENDING;
+
+    /**
+     * 관리자 관련 코드
+     */
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "approved_by")
+    private User approvedBy;
+
+    private LocalDateTime approvedAt;
+
+    public void approve(User admin) {
+        if (this.approvalStatus != ApprovalStatus.PENDING) {
+            throw new BusinessException(ErrorCode.ALREADY_ACCEPTED);
+        }
+        this.approvalStatus = ApprovalStatus.APPROVED;
+        this.approvedBy = admin;
+        this.approvedAt = LocalDateTime.now();
+    }
+
+    public void reject(User admin) {
+        if (this.approvalStatus != ApprovalStatus.PENDING) {
+            throw new BusinessException(ErrorCode.ALREADY_ACCEPTED);
+        }
+        this.approvalStatus = ApprovalStatus.REJECTED;
+        this.approvedBy = admin;
+        this.approvedAt = LocalDateTime.now();
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/repository/ProjectRepository.java b/src/main/java/org/farmsystem/homepage/domain/project/repository/ProjectRepository.java
new file mode 100644
index 00000000..ccefba4a
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/repository/ProjectRepository.java
@@ -0,0 +1,40 @@
+package org.farmsystem.homepage.domain.project.repository;
+
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.entity.ApprovalStatus;
+import org.farmsystem.homepage.domain.project.entity.Project;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface ProjectRepository extends JpaRepository<Project, Long> {
+
+    List<Project> findByApprovalStatus(ApprovalStatus status);
+
+    List<Project> findByUser_UserId(Long userId);
+
+
+    Page<Project> findByApprovalStatus(ApprovalStatus status, Pageable pageable);
+
+
+    Page<Project> findByApprovalStatusAndGeneration(
+            ApprovalStatus status,
+            Integer generation,
+            Pageable pageable
+    );
+
+    Page<Project> findByApprovalStatusAndTrack(
+            ApprovalStatus status,
+            Track track,
+            Pageable pageable
+    );
+
+    Page<Project> findByApprovalStatusAndGenerationAndTrack(
+            ApprovalStatus status,
+            Integer generation,
+            Track track,
+            Pageable pageable
+    );
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/service/AdminProjectService.java b/src/main/java/org/farmsystem/homepage/domain/project/service/AdminProjectService.java
new file mode 100644
index 00000000..2308a11e
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/service/AdminProjectService.java
@@ -0,0 +1,90 @@
+package org.farmsystem.homepage.domain.project.service;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectApprovalResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.domain.project.entity.ApprovalStatus;
+import org.farmsystem.homepage.domain.project.entity.Project;
+import org.farmsystem.homepage.domain.project.repository.ProjectRepository;
+import org.farmsystem.homepage.domain.user.entity.User;
+import org.farmsystem.homepage.domain.user.repository.UserRepository;
+import org.farmsystem.homepage.global.error.exception.EntityNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.farmsystem.homepage.global.error.ErrorCode.PROJECT_NOT_FOUND;
+import static org.farmsystem.homepage.global.error.ErrorCode.USER_NOT_FOUND;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class AdminProjectService {
+
+    private final ProjectRepository projectRepository;
+    private final UserRepository userRepository;
+
+    public ProjectApprovalResponseDTO approveProject(Long projectId, Long userId) {
+        Project project = projectRepository.findById(projectId)
+                .orElseThrow(() -> new EntityNotFoundException(PROJECT_NOT_FOUND));
+
+        User admin = userRepository.findById(userId)
+                .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
+
+        project.approve(admin);
+
+        return new ProjectApprovalResponseDTO(
+                project.getProjectId(),
+                project.getTitle(),
+                project.getApprovalStatus().name(),
+                admin.getName(),
+                project.getApprovedAt()
+        );
+    }
+
+    public ProjectApprovalResponseDTO rejectProject(Long projectId, Long userId) {
+        Project project = projectRepository.findById(projectId)
+                .orElseThrow(() -> new EntityNotFoundException(PROJECT_NOT_FOUND));
+
+        User admin = userRepository.findById(userId)
+                .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
+
+        project.reject(admin);
+
+        return new ProjectApprovalResponseDTO(
+                project.getProjectId(),
+                project.getTitle(),
+                project.getApprovalStatus().name(),
+                admin.getName(),
+                project.getApprovedAt()
+        );
+    }
+
+    /**
+     * 관리자 - 승인 대기 중인 프로젝트 목록 조회
+     */
+    @Transactional(readOnly = true)
+    public List<ProjectResponseDTO> getPendingProjects() {
+        return projectRepository.findByApprovalStatus(ApprovalStatus.PENDING).stream()
+                .map(this::toDTO)
+                .toList();
+    }
+
+    private ProjectResponseDTO toDTO(Project project) {
+        return new ProjectResponseDTO(
+                project.getProjectId(),
+                project.getTitle(),
+                project.getIntroduction(),
+                project.getContent(),
+                project.getThumbnailImageUrl(),
+                project.getBodyImageUrl(),
+                project.getGithubLink(),
+                project.getDeploymentLink(),
+                project.getResourceLink(),
+                project.getParticipants(),
+                project.getApprovalStatus().name(),
+                project.getTrack()
+        );
+    }
+}
diff --git a/src/main/java/org/farmsystem/homepage/domain/project/service/ProjectService.java b/src/main/java/org/farmsystem/homepage/domain/project/service/ProjectService.java
new file mode 100644
index 00000000..1f57c1d8
--- /dev/null
+++ b/src/main/java/org/farmsystem/homepage/domain/project/service/ProjectService.java
@@ -0,0 +1,89 @@
+package org.farmsystem.homepage.domain.project.service;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.dto.request.ProjectRequestDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectSimpleResponseDTO;
+import org.farmsystem.homepage.domain.project.entity.ApprovalStatus;
+import org.farmsystem.homepage.domain.project.entity.Project;
+import org.farmsystem.homepage.domain.project.repository.ProjectRepository;
+import org.farmsystem.homepage.domain.user.entity.User;
+import org.farmsystem.homepage.domain.user.repository.UserRepository;
+import org.farmsystem.homepage.global.error.exception.EntityNotFoundException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.farmsystem.homepage.global.error.ErrorCode.PROJECT_NOT_FOUND;
+import static org.farmsystem.homepage.global.error.ErrorCode.USER_NOT_FOUND;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProjectService {
+
+    private final ProjectRepository projectRepository;
+    private final UserRepository userRepository;
+
+    @Transactional
+    public void applyForProject(Long userId, ProjectRequestDTO request) {
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
+
+        Project project = Project.builder()
+                .title(request.title())
+                .introduction(request.introduction())
+                .content(request.content())
+                .thumbnailImageUrl(request.thumbnailImageUrl())
+                .bodyImageUrl(request.bodyImageUrl())
+                .githubLink(request.githubLink())
+                .deploymentLink(request.deploymentLink())
+                .resourceLink(request.resourceLink())
+                .user(user)
+                .generation(user.getGeneration())
+                .track(user.getTrack())
+                .participants(request.participants())
+                .approvalStatus(ApprovalStatus.PENDING)
+                .build();
+
+        projectRepository.save(project);
+    }
+
+    @Transactional(readOnly = true)
+    public List<ProjectResponseDTO> getMyProjects(Long userId) {
+        return projectRepository.findByUser_UserId(userId).stream()
+                .map(ProjectResponseDTO::fromEntity)
+                .toList();
+    }
+
+    @Transactional(readOnly = true)
+    public ProjectResponseDTO getProjectDetail(Long projectId) {
+        Project project = projectRepository.findById(projectId)
+                .orElseThrow(() -> new EntityNotFoundException(PROJECT_NOT_FOUND));
+
+        return ProjectResponseDTO.fromEntity(project);
+    }
+
+    public Page<ProjectSimpleResponseDTO> getFilteredProjects(Integer generation, Track track, int page, int size) {
+        Pageable pageable = PageRequest.of(page, size);
+        Page<Project> projects;
+
+        if (generation != null && track != null) {
+            projects = projectRepository.findByApprovalStatusAndGenerationAndTrack(ApprovalStatus.APPROVED, generation, track, pageable);
+        } else if (generation != null) {
+            projects = projectRepository.findByApprovalStatusAndGeneration(ApprovalStatus.APPROVED, generation, pageable);
+        } else if (track != null) {
+            projects = projectRepository.findByApprovalStatusAndTrack(ApprovalStatus.APPROVED, track, pageable);
+        } else {
+            projects = projectRepository.findByApprovalStatus(ApprovalStatus.APPROVED, pageable);
+        }
+
+        return projects.map(ProjectSimpleResponseDTO::fromEntity);
+    }
+
+}
diff --git a/src/main/java/org/farmsystem/homepage/global/error/ErrorCode.java b/src/main/java/org/farmsystem/homepage/global/error/ErrorCode.java
index 4cc0466c..8208ecd5 100644
--- a/src/main/java/org/farmsystem/homepage/global/error/ErrorCode.java
+++ b/src/main/java/org/farmsystem/homepage/global/error/ErrorCode.java
@@ -111,6 +111,7 @@ public enum ErrorCode {
     BLOG_NOT_FOUND(HttpStatus.NOT_FOUND, "블로그를 찾을 수 없습니다."),
     BLOG_DUPLICATED(HttpStatus.CONFLICT, "이미 신청 처리된 블로그입니다."),
     ALREADY_ACCEPTED(HttpStatus.BAD_REQUEST, "이미 승인 처리된 블로그입니다."),
+    PROJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "프로젝트를 찾을 수 없습니다."),
 
     /**
      * Notification Error


Reviews:
user: login: hysong4u
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T10:19:36Z
user: login: hysong4u
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T10:30:11Z
user: login: hysong4u
body: 컨트롤러들 성공 응답 객체 <SuccessResponse<?>> 요거 추가하는 거 수정해주면 좋을 거 같습니다
고생했어요~~
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T10:36:16Z
user: login: hysong4u
body: 고생했어요 최고..!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T04:17:55Z

Review comments:
diff_hunk: @@ -0,0 +1,121 @@
+package org.farmsystem.homepage.domain.project.service;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.dto.request.ProjectRequestDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectSimpleResponseDTO;
+import org.farmsystem.homepage.domain.project.entity.ApprovalStatus;
+import org.farmsystem.homepage.domain.project.entity.Project;
+import org.farmsystem.homepage.domain.project.repository.ProjectRepository;
+import org.farmsystem.homepage.domain.user.entity.User;
+import org.farmsystem.homepage.domain.user.repository.UserRepository;
+import org.farmsystem.homepage.global.error.exception.EntityNotFoundException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.farmsystem.homepage.global.error.ErrorCode.PROJECT_NOT_FOUND;
+import static org.farmsystem.homepage.global.error.ErrorCode.USER_NOT_FOUND;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProjectService {
+
+    private final ProjectRepository projectRepository;
+    private final UserRepository userRepository;
+
+    @Transactional
+    public void applyForProject(Long userId, ProjectRequestDTO request) {
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
+
+        Project project = Project.builder()
+                .title(request.title())
+                .introduction(request.introduction())
+                .content(request.content())
+                .thumbnailImageUrl(request.thumbnailImageUrl())
+                .bodyImageUrl(request.bodyImageUrl())
+                .githubLink(request.githubLink())
+                .deploymentLink(request.deploymentLink())
+                .resourceLink(request.resourceLink())
+                .user(user)
+                .generation(user.getGeneration())
+                .track(user.getTrack())
+                .participants(request.participants())
+                .approvalStatus(ApprovalStatus.PENDING)
+                .build();
+
+        projectRepository.save(project);
+    }
+
+    @Transactional(readOnly = true)
+    public List<ProjectResponseDTO> getMyProjects(Long userId) {
+        return projectRepository.findByUser_UserId(userId).stream()
+                .map(this::toDTO)
+                .toList();
+    }
+
+
+    public Page<ProjectSimpleResponseDTO> getFilteredProjects(Integer generation, Track track, int page, int size) {
+        Pageable pageable = PageRequest.of(page, size);
+        Page<Project> projects;
+
+        if (generation != null && track != null) {
+            projects = projectRepository.findByApprovalStatusAndGenerationAndTrack(ApprovalStatus.APPROVED, generation, track, pageable);
+        } else if (generation != null) {
+            projects = projectRepository.findByApprovalStatusAndGeneration(ApprovalStatus.APPROVED, generation, pageable);
+        } else if (track != null) {
+            projects = projectRepository.findByApprovalStatusAndTrack(ApprovalStatus.APPROVED, track, pageable);
+        } else {
+            projects = projectRepository.findByApprovalStatus(ApprovalStatus.APPROVED, pageable);
+        }
+
+        return projects.map(this::toSimpleDTO);
+    }
+
+    @Transactional(readOnly = true)
+    public ProjectResponseDTO getProjectDetail(Long projectId) {
+        Project project = projectRepository.findById(projectId)
+                .orElseThrow(() -> new EntityNotFoundException(PROJECT_NOT_FOUND));
+
+        return toDTO(project);
+    }
+
+
+
+
+    private ProjectResponseDTO toDTO(Project project) {
path: src/main/java/org/farmsystem/homepage/domain/project/service/ProjectService.java
user: login: hysong4u
body: 요거 엔티티-> DTO 변환 로직은 서비스 클래스에 해도 괜찮긴한데 DTO 클래스 내부에 정의하면 가독성 & 수정관리 측면(response값을 바꾸고싶으면 서비스 클래스 안건드리고 DTO 클래스만 보면됨)에 편해서 추천드립니다!
created_at: 2025-03-22T10:19:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,121 @@
+package org.farmsystem.homepage.domain.project.service;
+
+import lombok.RequiredArgsConstructor;
+import org.farmsystem.homepage.domain.common.entity.Track;
+import org.farmsystem.homepage.domain.project.dto.request.ProjectRequestDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectResponseDTO;
+import org.farmsystem.homepage.domain.project.dto.response.ProjectSimpleResponseDTO;
+import org.farmsystem.homepage.domain.project.entity.ApprovalStatus;
+import org.farmsystem.homepage.domain.project.entity.Project;
+import org.farmsystem.homepage.domain.project.repository.ProjectRepository;
+import org.farmsystem.homepage.domain.user.entity.User;
+import org.farmsystem.homepage.domain.user.repository.UserRepository;
+import org.farmsystem.homepage.global.error.exception.EntityNotFoundException;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.farmsystem.homepage.global.error.ErrorCode.PROJECT_NOT_FOUND;
+import static org.farmsystem.homepage.global.error.ErrorCode.USER_NOT_FOUND;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class ProjectService {
+
+    private final ProjectRepository projectRepository;
+    private final UserRepository userRepository;
+
+    @Transactional
+    public void applyForProject(Long userId, ProjectRequestDTO request) {
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND));
+
+        Project project = Project.builder()
+                .title(request.title())
+                .introduction(request.introduction())
+                .content(request.content())
+                .thumbnailImageUrl(request.thumbnailImageUrl())
+                .bodyImageUrl(request.bodyImageUrl())
+                .githubLink(request.githubLink())
+                .deploymentLink(request.deploymentLink())
+                .resourceLink(request.resourceLink())
+                .user(user)
+                .generation(user.getGeneration())
+                .track(user.getTrack())
+                .participants(request.participants())
+                .approvalStatus(ApprovalStatus.PENDING)
+                .build();
+
+        projectRepository.save(project);
+    }
+
+    @Transactional(readOnly = true)
+    public List<ProjectResponseDTO> getMyProjects(Long userId) {
+        return projectRepository.findByUser_UserId(userId).stream()
+                .map(this::toDTO)
+                .toList();
+    }
+
+
+    public Page<ProjectSimpleResponseDTO> getFilteredProjects(Integer generation, Track track, int page, int size) {
path: src/main/java/org/farmsystem/homepage/domain/project/service/ProjectService.java
user: login: hysong4u
body: 페이징 필터링 저도 쓰고있어서 구현 방식에 대해 고민중인데,,,(나는 JPQL로 동적쿼리 썼음) 
뭔가 paging 필터링을 할 수 있는 공통 클래스가 하나 있으면 졸겠다는 생각.. (소은이꺼도 참고해서 이 방식은 제가 좀 더 고민을 해볼게여)
created_at: 2025-03-22T10:30:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: saokiritoni
created_at: 2025-03-22T17:08:28Z
body: > 컨트롤러들 성공 응답 객체 <SuccessResponse<?>> 요거 추가하는 거 수정해주면 좋을 거 같습니다
> 
> 고생했어요~~

아 헣 맞다 알게뜹니다~~~!
author_association: MEMBER
user: login: saokiritoni
created_at: 2025-03-25T11:21:10Z
body: 다했더요
author_association: MEMBER