Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,83 @@
# ClubApplicationPlatform_Server
[파일 역할 정리]
📌 공통 모듈

ApiPath.java → 모든 API 엔드포인트 경로를 상수로 정의 (공지/FAQ, 동아리 신청 등).
Response.java → 공통 응답 객체(상태 코드, 데이터, 메시지, 타임스탬프 포함).
NotFoundException.java → 데이터 미존재 시 예외 정의.
GlobalExceptionHandler.java → 예외를 공통적으로 처리(404/500 반환).
JwtProvider.java → JWT 스텁, "Bearer 이메일" 형식에서 이메일 추출.
SecurityConfiguration.java → Spring Security 설정 (CSRF, FormLogin 비활성화, 세션 정책).

📌 동아리 신청 (ClubApplication) 도메인

ClubApplication.java → 신청 엔티티 (신청자, 클럽, 학과, 질문답변, 상태 등).
ClubStatus.java → 신청 상태(PENDING, PASSED, FAILED, CONFIRMED, DECLINED).
QuestionAnswer.java → 지원자가 입력한 질문/답변 구조.
ClubApplicationDto.java → DTO (엔티티 ↔ 응답 변환).
ClubSummary.java → 학과별 클럽 요약 DTO (id, 이름).
ClubApplicationRepository.java → 인메모리 저장소(Map), CRUD, 클럽/학과별 조회 지원.
ClubApplicationService.java → 비즈니스 로직 (신청 등록/조회/수정/취소, 합격 여부, 확정/철회, 추가 합격).
ClubApplicationController.java → REST API 제공 (POST/GET/PUT/PATCH/DELETE).

📌 공지 / FAQ (Notice) 도메인

Notice.java → 공지 엔티티 (id, clubId, 제목, 내용, 작성일, 타입(FAQ/CLUB_NOTICE)).

NoticeResponse.java → 응답 DTO (엔티티 → 클라이언트 응답 변환).
NoticeCreateRequest.java → 공지/FAQ 등록 요청 DTO.
NoticeRepository.java → 인메모리 저장소(List), 샘플 데이터 4건 포함, FAQ/공지 CRUD.
NoticeService.java → 공지/FAQ 비즈니스 로직 (FAQ 목록, 상세조회, 등록, 클럽 공지).
NoticeController.java → REST API 제공 (FAQ 목록 조회, 공지 상세 조회, FAQ 등록).

📌 부트스트랩
DpApplication.java → Spring Boot 메인 실행 클래스.

📌 END POINT 기능 정리
🔹 동아리 신청 (ClubApplicationController)
기본 CRUD

POST /api/applications
→ 신규 동아리 신청 등록

GET /api/applications
→ 전체 신청 목록 조회

GET /api/applications/{applicationId}
→ 특정 신청 상세 조회

DELETE /api/applications/{applicationId}
→ 신청 취소(삭제)

PUT /api/applications
→ 신청 정보 전체 수정

PATCH /api/applications/{applicationId}
→ 신청 정보 일부 수정

추가 기능

GET /api/clubs/{clubId}/applications
→ 특정 클럽의 신청자 목록 조회

PATCH /api/applications/{applicationId}/result
→ 합격 여부 설정 (result=pass|fail, message 포함, JSON Body 방식)

PATCH /api/applications/{applicationId}/confirm
→ 합격자 본인의 확정(confirm) / 철회(decline)

PATCH /api/applications/{applicationId}/additional-offer
→ 불합격자 추가 합격 통보 (FAILED → PASSED)

GET /api/departments/{departmentId}/clubs
→ 학과별 클럽 목록 조회

🔹 공지 / FAQ (NoticeController)

GET /api/notices
→ FAQ 목록 조회

GET /api/notices/{noticeId}
→ 공지 상세 조회 (FAQ/클럽공지 공통)

POST /api/notices
→ FAQ 등록
24 changes: 24 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,32 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Spring Boot Starters
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// ✅ Lombok (버전 명시 + annotationProcessor)
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'

testCompileOnly 'org.projectlombok:lombok:1.18.32'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.32'

developmentOnly 'org.springframework.boot:spring-boot-devtools'

runtimeOnly 'com.h2database:h2'
// runtimeOnly 'com.mysql:mysql-connector-j'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}



tasks.named('test') {
useJUnitPlatform()
}
Empty file modified gradlew
100644 → 100755
Empty file.
2 changes: 2 additions & 0 deletions src/main/java/com/JoinUs/dp/DpApplication.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.JoinUs.dp;

// git 강의 sdf

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/JoinUs/dp/common/api/ApiPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// src/main/java/com/JoinUs/dp/global/common/api/ApiPath.java
package com.JoinUs.dp.common.api;

public final class ApiPath {
private ApiPath() {}

// 공지/FAQ
public static final String NOTICE_PATH = "/api/notices";
public static final String CLUB_NOTICE_PATH = "/api/clubs/{clubId}/notice";

// ✅ 동아리 신청
public static final String APPLICATIONS = "/api/applications";
public static final String CLUB_APPLICATIONS = "/api/clubs/{clubId}/applications";
public static final String DEPARTMENT_CLUBS = "/api/departments/{departmentId}/clubs";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.JoinUs.dp.common.exception;


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.JoinUs.dp.common.response.Response;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Response<Void>> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new Response<>(HttpStatus.NOT_FOUND.value(), null, ex.getMessage()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Response<Void>> handleOther(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new Response<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), null, "서버 오류가 발생했습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.JoinUs.dp.common.exception;


public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) { super(msg); }
}
27 changes: 27 additions & 0 deletions src/main/java/com/JoinUs/dp/common/response/Response.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.JoinUs.dp.common.response;


import java.time.LocalDateTime;

public class Response<T> {
private int status;
private T data;
private String message;
private LocalDateTime timestamp = LocalDateTime.now();

public Response(int status, T data, String message) {
this.status = status;
this.data = data;
this.message = message;
}

public int getStatus() { return status; }
public T getData() { return data; }
public String getMessage() { return message; }
public LocalDateTime getTimestamp() { return timestamp; }

public void setStatus(int status) { this.status = status; }
public void setData(T data) { this.data = data; }
public void setMessage(String message) { this.message = message; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
18 changes: 18 additions & 0 deletions src/main/java/com/JoinUs/dp/common/security/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.JoinUs.dp.common.security;


import org.springframework.stereotype.Component;

// 실제 JWT 파싱 대신 "Bearer 이메일" 형식에서 이메일만 잘라내는 스텁 예시
@Component
public class JwtProvider {
public String extractEmail(String tokenHeader) {
if (tokenHeader == null) return null;
// 예: "Bearer user@example.com"
String prefix = "Bearer ";
if (tokenHeader.startsWith(prefix)) {
return tokenHeader.substring(prefix.length()).trim();
}
return null;
}
}
116 changes: 116 additions & 0 deletions src/main/java/com/JoinUs/dp/controller/ClubApplicationController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// src/main/java/com/JoinUs/dp/global/domain/application/controller/ClubApplicationController.java
package com.JoinUs.dp.controller;

import java.util.List;
import java.util.Map;

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

import com.JoinUs.dp.common.api.ApiPath;
import com.JoinUs.dp.common.response.Response;
import com.JoinUs.dp.dto.ClubApplicationDto;
import com.JoinUs.dp.dto.ClubSummary;
import com.JoinUs.dp.service.ClubApplicationService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class ClubApplicationController {

private final ClubApplicationService service;



// 동아리 신청 등록: POST /api/applications
@PostMapping(ApiPath.APPLICATIONS)
public ResponseEntity<Response<ClubApplicationDto>> apply(@RequestBody ClubApplicationDto req) {
ClubApplicationDto saved = service.apply(req);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new Response<>(HttpStatus.CREATED.value(), saved, "동아리 신청 등록 완료"));
}

// 신청 목록 전체 조회: GET /api/applications
@GetMapping(ApiPath.APPLICATIONS)
public ResponseEntity<Response<List<ClubApplicationDto>>> findAll() {
List<ClubApplicationDto> list = service.findAll();
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), list, "신청 목록 조회 성공"));
}

// 신청 상세 조회: GET /api/applications/{applicationId}
@GetMapping(ApiPath.APPLICATIONS + "/{applicationId}")
public ResponseEntity<Response<ClubApplicationDto>> findById(@PathVariable("applicationId") Long applicationId) {
ClubApplicationDto dto = service.findById(applicationId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), dto, "신청 상세 조회 성공"));
}

// 신청 취소(삭제): DELETE /api/applications/{applicationId}
@DeleteMapping(ApiPath.APPLICATIONS + "/{applicationId}")
public ResponseEntity<Response<Void>> cancel(@PathVariable("applicationId") Long applicationId) {
service.cancel(applicationId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), null, "신청 취소(삭제) 완료"));
}

// 신청 정보 수정(전체): PUT /api/applications
@PutMapping(ApiPath.APPLICATIONS)
public ResponseEntity<Response<ClubApplicationDto>> update(@RequestBody ClubApplicationDto req) {
ClubApplicationDto updated = service.update(req);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), updated, "신청 정보 수정 완료"));
}

// 신청 정보 일부 수정: PATCH /api/applications/{applicationId}
@PatchMapping(ApiPath.APPLICATIONS + "/{applicationId}")
public ResponseEntity<Response<ClubApplicationDto>> partialUpdate(
@PathVariable("applicationId") Long applicationId,
@RequestBody Map<String, Object> updates) {
ClubApplicationDto updated = service.partialUpdate(applicationId, updates);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), updated, "신청 정보 일부 수정 완료"));
}

// 특정 클럽의 신청자 목록 조회: GET /api/clubs/{clubId}/applications
@GetMapping(ApiPath.CLUB_APPLICATIONS)
public ResponseEntity<Response<List<ClubApplicationDto>>> findByClubId(@PathVariable("clubId") Long clubId) {
List<ClubApplicationDto> list = service.findByClubId(clubId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), list, "클럽별 신청자 목록 조회 성공"));
}

// 합격 여부 설정(동아리장 전용): PATCH /api/applications/{applicationId}/result
@PatchMapping(ApiPath.APPLICATIONS + "/{applicationId}/result")
public ResponseEntity<Response<ClubApplicationDto>> setResult(
@PathVariable("applicationId") Long applicationId,
@RequestBody Map<String, String> body) {

String result = body.get("result");
String message = body.get("message");
ClubApplicationDto updated = service.setResult(applicationId, result, message);

return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), updated, "합격 여부 설정 완료"));
}

// 합격자 최종 확정/철회(본인): PATCH /api/applications/{applicationId}/confirm
@PatchMapping(ApiPath.APPLICATIONS + "/{applicationId}/confirm")
public ResponseEntity<Response<ClubApplicationDto>> confirmOrDecline(
@PathVariable("applicationId") Long applicationId,
@RequestBody Map<String, String> body) {

String action = body.get("action");
ClubApplicationDto updated = service.confirmOrDecline(applicationId, action);

return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), updated, "확정/철회 처리 완료"));
}

// 불합격자 추가 합격 통보(동아리장 전용): PATCH /api/applications/{applicationId}/additional-offer
@PatchMapping(ApiPath.APPLICATIONS + "/{applicationId}/additional-offer")
public ResponseEntity<Response<ClubApplicationDto>> additionalOffer(@PathVariable("applicationId") Long applicationId) {
ClubApplicationDto updated = service.additionalOffer(applicationId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), updated, "추가 합격 통보 완료"));
}

// 학과별 동아리 목록 조회: GET /api/departments/{departmentId}/clubs
@GetMapping(ApiPath.DEPARTMENT_CLUBS)
public ResponseEntity<Response<List<ClubSummary>>> getClubsByDepartment(@PathVariable("departmentId") Long departmentId) {
List<ClubSummary> list = service.getClubsByDepartment(departmentId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), list, "학과별 클럽 목록 조회 성공"));
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/JoinUs/dp/controller/NoticeController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// src/main/java/com/JoinUs/dp/global/domain/notice/controller/NoticeController.java
package com.JoinUs.dp.controller;

import com.JoinUs.dp.common.api.ApiPath;
import com.JoinUs.dp.common.response.Response;
import com.JoinUs.dp.dto.NoticeCreateRequest;
import com.JoinUs.dp.dto.NoticeResponse;
import com.JoinUs.dp.service.NoticeService;

import lombok.RequiredArgsConstructor;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(ApiPath.NOTICE_PATH)
@RequiredArgsConstructor
public class NoticeController {

private final NoticeService noticeService; // ✅ new 금지, DI 받기


@GetMapping
public ResponseEntity<Response<List<NoticeResponse>>> getFaqs() {
List<NoticeResponse> list = noticeService.getFaqList();
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), list, "FAQ 목록 조회 성공"));
}

@GetMapping("/{noticeId}")
public ResponseEntity<Response<NoticeResponse>> getDetail(@PathVariable("noticeId") Long noticeId) {
NoticeResponse dto = noticeService.getNoticeDetail(noticeId);
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), dto, "공지 상세 조회 성공"));
}
// ✅ FAQ 등록 (중복 경로 제거)
@PostMapping
public ResponseEntity<Response<NoticeResponse>> createFaq(@RequestBody NoticeCreateRequest req) {
NoticeResponse saved = noticeService.createFaq(req.getTitle(), req.getContent());
return ResponseEntity.ok(new Response<>(HttpStatus.OK.value(), saved, "FAQ 등록 성공"));
}

}
7 changes: 7 additions & 0 deletions src/main/java/com/JoinUs/dp/controller/Snippet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.JoinUs.dp.controller;

public class Snippet {
public static void main(String[] args) {
}
}

Loading