From a999617844eb47f69e5f75e2044a362a1f58fcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 10:57:27 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20@Auth=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=9D=B8=EA=B0=80=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EC=85=89=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/global/auth/annotation/Auth.java | 16 ++++ .../interceptor/AuthorizationInterceptor.java | 74 +++++++++++++++++++ .../interceptor/LoginCheckInterceptor.java | 6 ++ .../konect/global/code/ApiResponseCode.java | 1 + .../agit/konect/global/config/WebConfig.java | 6 ++ 5 files changed, 103 insertions(+) create mode 100644 src/main/java/gg/agit/konect/global/auth/annotation/Auth.java create mode 100644 src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java diff --git a/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java b/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java new file mode 100644 index 00000000..082ac212 --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java @@ -0,0 +1,16 @@ +package gg.agit.konect.global.auth.annotation; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import gg.agit.konect.domain.user.enums.UserRole; + +@Target(METHOD) +@Retention(RUNTIME) +public @interface Auth { + + UserRole[] roles(); +} diff --git a/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java b/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java new file mode 100644 index 00000000..088def87 --- /dev/null +++ b/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java @@ -0,0 +1,74 @@ +package gg.agit.konect.global.auth.interceptor; + +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.auth.annotation.Auth; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AuthorizationInterceptor implements HandlerInterceptor { + + private final UserRepository userRepository; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + return true; + } + + if (!(handler instanceof HandlerMethod handlerMethod)) { + return true; + } + + if (Boolean.TRUE.equals(request.getAttribute(LoginCheckInterceptor.PUBLIC_ENDPOINT_ATTRIBUTE))) { + return true; + } + + Auth auth = findAuthAnnotation(handlerMethod); + + if (auth == null) { + return true; + } + + Object userId = request.getAttribute(LoginCheckInterceptor.AUTHENTICATED_USER_ID_ATTRIBUTE); + + if (!(userId instanceof Integer id)) { + throw CustomException.of(ApiResponseCode.INVALID_SESSION); + } + + validateRole(id, auth); + + return true; + } + + private Auth findAuthAnnotation(HandlerMethod handlerMethod) { + Auth methodAnnotation = handlerMethod.getMethodAnnotation(Auth.class); + if (methodAnnotation != null) { + return methodAnnotation; + } + + return handlerMethod.getBeanType().getAnnotation(Auth.class); + } + + private void validateRole(Integer userId, Auth auth) { + User user = userRepository.getById(userId); + + for (var allowedRole : auth.roles()) { + if (user.getRole() == allowedRole) { + return; + } + } + + throw CustomException.of(ApiResponseCode.FORBIDDEN_ROLE_ACCESS); + } +} diff --git a/src/main/java/gg/agit/konect/global/auth/interceptor/LoginCheckInterceptor.java b/src/main/java/gg/agit/konect/global/auth/interceptor/LoginCheckInterceptor.java index 03690b51..09a02140 100644 --- a/src/main/java/gg/agit/konect/global/auth/interceptor/LoginCheckInterceptor.java +++ b/src/main/java/gg/agit/konect/global/auth/interceptor/LoginCheckInterceptor.java @@ -15,6 +15,9 @@ @Component public class LoginCheckInterceptor implements HandlerInterceptor { + public static final String AUTHENTICATED_USER_ID_ATTRIBUTE = "authenticatedUserId"; + public static final String PUBLIC_ENDPOINT_ATTRIBUTE = "publicEndpoint"; + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (HttpMethod.OPTIONS.matches(request.getMethod())) { @@ -26,6 +29,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } if (isPublicEndpoint(handlerMethod)) { + request.setAttribute(PUBLIC_ENDPOINT_ATTRIBUTE, true); return true; } @@ -36,6 +40,8 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons throw CustomException.of(ApiResponseCode.INVALID_SESSION); } + request.setAttribute(AUTHENTICATED_USER_ID_ATTRIBUTE, userId); + return true; } diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index dcd51722..badde3b8 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -39,6 +39,7 @@ public enum ApiResponseCode { FORBIDDEN_CLUB_MEMBER_ACCESS(HttpStatus.FORBIDDEN, "동아리 멤버 조회 권한이 없습니다."), FORBIDDEN_COUNCIL_NOTICE_ACCESS(HttpStatus.FORBIDDEN, "총동아리연합회 공지사항 조회 권한이 없습니다."), FORBIDDEN_CLUB_RECRUITMENT_CREATE(HttpStatus.FORBIDDEN, "동아리 모집 공고를 생성할 권한이 없습니다."), + FORBIDDEN_ROLE_ACCESS(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."), // 404 Not Found (리소스를 찾을 수 없음) NO_HANDLER_FOUND(HttpStatus.NOT_FOUND, "유효하지 않은 API 경로입니다."), diff --git a/src/main/java/gg/agit/konect/global/config/WebConfig.java b/src/main/java/gg/agit/konect/global/config/WebConfig.java index bab22442..7ceea7e4 100644 --- a/src/main/java/gg/agit/konect/global/config/WebConfig.java +++ b/src/main/java/gg/agit/konect/global/config/WebConfig.java @@ -10,6 +10,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import gg.agit.konect.global.auth.interceptor.LoginCheckInterceptor; +import gg.agit.konect.global.auth.interceptor.AuthorizationInterceptor; import gg.agit.konect.global.auth.resolver.LoginUserArgumentResolver; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ public class WebConfig implements WebMvcConfigurer { private final CorsProperties corsProperties; private final LoginCheckInterceptor loginCheckInterceptor; + private final AuthorizationInterceptor authorizationInterceptor; private final LoginUserArgumentResolver loginUserArgumentResolver; @Override @@ -43,6 +45,10 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor) .addPathPatterns("/**") .excludePathPatterns(SecurityPaths.PUBLIC_PATHS); + + registry.addInterceptor(authorizationInterceptor) + .addPathPatterns("/**") + .excludePathPatterns(SecurityPaths.PUBLIC_PATHS); } @Override From 95cff863808e29979998747b67763d56b011d049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 16:56:25 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=9D=BC=EA=B4=84=20=EC=83=9D=EC=84=B1/?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EB=AA=85=EC=84=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/controller/AdminScheduleApi.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java new file mode 100644 index 00000000..7130a2c7 --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java @@ -0,0 +1,39 @@ +package gg.agit.konect.admin.schedule.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import gg.agit.konect.admin.schedule.dto.AdminScheduleCreateRequest; +import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertRequest; +import gg.agit.konect.domain.user.enums.UserRole; +import gg.agit.konect.global.auth.annotation.Auth; +import gg.agit.konect.global.auth.annotation.UserId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "(Admin) Schedule: 일정", description = "어드민 일정 API") +@RequestMapping("/admin/schedule") +public interface AdminScheduleApi { + + @Operation(summary = "일정을 생성한다.") + @PostMapping + @Auth(roles = {UserRole.ADMIN}) + ResponseEntity createSchedule( + @Valid @RequestBody AdminScheduleCreateRequest request, + @UserId Integer userId + ); + + @Operation(summary = "일정을 일괄 생성/수정한다.", description = """ + scheduleId가 없으면 신규 생성, 있으면 해당 일정 수정입니다. + """) + @PutMapping("/batch") + @Auth(roles = {UserRole.ADMIN}) + ResponseEntity upsertSchedules( + @Valid @RequestBody AdminScheduleUpsertRequest request, + @UserId Integer userId + ); +} From a6f77054560a6804b756447b77918936f1d8a22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 16:56:57 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=9D=BC=EA=B4=84=20=EC=83=9D=EC=84=B1/?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminScheduleController.java | 33 +++++++ .../dto/AdminScheduleCreateRequest.java | 32 +++++++ .../dto/AdminScheduleUpsertItemRequest.java | 36 ++++++++ .../dto/AdminScheduleUpsertRequest.java | 19 ++++ .../service/AdminScheduleService.java | 91 +++++++++++++++++++ .../domain/schedule/model/Schedule.java | 26 ++++++ .../schedule/model/UniversitySchedule.java | 10 ++ .../repository/ScheduleRepository.java | 5 + .../UniversityScheduleRepository.java | 33 +++++++ .../konect/global/code/ApiResponseCode.java | 1 + 10 files changed, 286 insertions(+) create mode 100644 src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java create mode 100644 src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java create mode 100644 src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java create mode 100644 src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java create mode 100644 src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java create mode 100644 src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java new file mode 100644 index 00000000..096b5675 --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java @@ -0,0 +1,33 @@ +package gg.agit.konect.admin.schedule.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import gg.agit.konect.admin.schedule.dto.AdminScheduleCreateRequest; +import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertRequest; +import gg.agit.konect.admin.schedule.service.AdminScheduleService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin/schedule") +public class AdminScheduleController implements AdminScheduleApi { + + + private final AdminScheduleService adminScheduleService; + + @Override + public ResponseEntity createSchedule(AdminScheduleCreateRequest request, Integer userId) { + adminScheduleService.createSchedule(request, userId); + + return ResponseEntity.ok().build(); + } + + @Override + public ResponseEntity upsertSchedules(AdminScheduleUpsertRequest request, Integer userId) { + adminScheduleService.upsertSchedules(request, userId); + + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java new file mode 100644 index 00000000..842d25f1 --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java @@ -0,0 +1,32 @@ +package gg.agit.konect.admin.schedule.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import gg.agit.konect.domain.schedule.model.ScheduleType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AdminScheduleCreateRequest( + @NotBlank + @Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED) + String title, + + @NotNull + @Schema(description = "일정 시작 일시", example = "2025-12-22 00:00:00", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") + LocalDateTime startedAt, + + @NotNull + @Schema(description = "일정 종료 일시", example = "2026-02-27 23:59:59", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") + LocalDateTime endedAt, + + @Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED) + ScheduleType scheduleType +) { +} diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java new file mode 100644 index 00000000..d1d05a92 --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java @@ -0,0 +1,36 @@ +package gg.agit.konect.admin.schedule.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import gg.agit.konect.domain.schedule.model.ScheduleType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AdminScheduleUpsertItemRequest( + @Schema(description = "수정할 일정 ID (없으면 신규 생성)", example = "1") + Integer scheduleId, + + @NotBlank + @Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED) + String title, + + @NotNull + @Schema(description = "일정 시작 일시", example = "2025-12-22 00:00:00", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") + LocalDateTime startedAt, + + @NotNull + @Schema(description = "일정 종료 일시", example = "2026-02-27 23:59:59", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") + LocalDateTime endedAt, + + @NotNull + @Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED) + ScheduleType scheduleType +) { +} diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java new file mode 100644 index 00000000..e517fe4c --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java @@ -0,0 +1,19 @@ +package gg.agit.konect.admin.schedule.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public record AdminScheduleUpsertRequest( + @NotNull + @NotEmpty + @Valid + @Schema(description = "생성/수정할 일정 목록", requiredMode = REQUIRED) + List schedules +) { +} diff --git a/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java new file mode 100644 index 00000000..d213ade9 --- /dev/null +++ b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java @@ -0,0 +1,91 @@ +package gg.agit.konect.admin.schedule.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.admin.schedule.dto.AdminScheduleCreateRequest; +import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertItemRequest; +import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertRequest; +import gg.agit.konect.domain.schedule.model.Schedule; +import gg.agit.konect.domain.schedule.model.UniversitySchedule; +import gg.agit.konect.domain.schedule.repository.ScheduleRepository; +import gg.agit.konect.domain.schedule.repository.UniversityScheduleRepository; +import gg.agit.konect.domain.university.model.University; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminScheduleService { + + private final UserRepository userRepository; + private final ScheduleRepository scheduleRepository; + private final UniversityScheduleRepository universityScheduleRepository; + + @Transactional + public void createSchedule(AdminScheduleCreateRequest request, Integer userId) { + User user = userRepository.getById(userId); + University university = user.getUniversity(); + + Schedule schedule = Schedule.of( + request.title(), + request.startedAt(), + request.endedAt(), + request.scheduleType() + ); + + Schedule savedSchedule = scheduleRepository.save(schedule); + + UniversitySchedule universitySchedule = UniversitySchedule.of( + savedSchedule, + university + ); + + universityScheduleRepository.save(universitySchedule); + } + + @Transactional + public void upsertSchedules(AdminScheduleUpsertRequest request, Integer userId) { + User user = userRepository.getById(userId); + University university = user.getUniversity(); + + for (AdminScheduleUpsertItemRequest item : request.schedules()) { + if (item.scheduleId() == null) { + createUniversitySchedule(item, university); + continue; + } + + UniversitySchedule universitySchedule = universityScheduleRepository.getByIdAndUniversityId( + item.scheduleId(), + university.getId() + ); + + universitySchedule.getSchedule().update( + item.title(), + item.startedAt(), + item.endedAt(), + item.scheduleType() + ); + } + } + + private void createUniversitySchedule(AdminScheduleUpsertItemRequest item, University university) { + Schedule schedule = Schedule.of( + item.title(), + item.startedAt(), + item.endedAt(), + item.scheduleType() + ); + + Schedule savedSchedule = scheduleRepository.save(schedule); + + UniversitySchedule universitySchedule = UniversitySchedule.of( + savedSchedule, + university + ); + + universityScheduleRepository.save(universitySchedule); + } +} diff --git a/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java b/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java index 169e9cb4..d1c4b6ba 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java +++ b/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java @@ -63,6 +63,32 @@ private Schedule( this.scheduleType = scheduleType; } + public static Schedule of( + String title, + LocalDateTime startedAt, + LocalDateTime endedAt, + ScheduleType scheduleType + ) { + return Schedule.builder() + .title(title) + .startedAt(startedAt) + .endedAt(endedAt) + .scheduleType(scheduleType) + .build(); + } + + public void update( + String title, + LocalDateTime startedAt, + LocalDateTime endedAt, + ScheduleType scheduleType + ) { + this.title = title; + this.startedAt = startedAt; + this.endedAt = endedAt; + this.scheduleType = scheduleType; + } + public Integer calculateDDay(LocalDate today) { if (today.isBefore(this.startedAt.toLocalDate())) { return (int)ChronoUnit.DAYS.between(today, this.startedAt.toLocalDate()); diff --git a/src/main/java/gg/agit/konect/domain/schedule/model/UniversitySchedule.java b/src/main/java/gg/agit/konect/domain/schedule/model/UniversitySchedule.java index e6828393..3be3d3b7 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/model/UniversitySchedule.java +++ b/src/main/java/gg/agit/konect/domain/schedule/model/UniversitySchedule.java @@ -42,4 +42,14 @@ private UniversitySchedule( this.schedule = schedule; this.university = university; } + + public static UniversitySchedule of( + Schedule schedule, + University university + ) { + return UniversitySchedule.builder() + .schedule(schedule) + .university(university) + .build(); + } } diff --git a/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java b/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java index 45ea1632..b7b29ed7 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java +++ b/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; @@ -12,6 +13,10 @@ public interface ScheduleRepository extends Repository { + Optional findById(Integer id); + + Schedule save(Schedule schedule); + @Query(""" SELECT s FROM Schedule s diff --git a/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java b/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java new file mode 100644 index 00000000..d97e7a60 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java @@ -0,0 +1,33 @@ +package gg.agit.konect.domain.schedule.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.schedule.model.UniversitySchedule; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; + +public interface UniversityScheduleRepository extends Repository { + + void save(UniversitySchedule universitySchedule); + + @Query(""" + SELECT us + FROM UniversitySchedule us + JOIN FETCH us.schedule s + WHERE us.id = :scheduleId + AND us.university.id = :universityId + """) + Optional findByIdAndUniversityId( + @Param("scheduleId") Integer scheduleId, + @Param("universityId") Integer universityId + ); + + default UniversitySchedule getByIdAndUniversityId(Integer scheduleId, Integer universityId) { + return findByIdAndUniversityId(scheduleId, universityId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_SCHEDULE)); + } +} diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index badde3b8..2672601e 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -52,6 +52,7 @@ public enum ApiResponseCode { NOT_FOUND_COUNCIL(HttpStatus.NOT_FOUND, "총동아리연합회를 찾을 수 없습니다."), NOT_FOUND_COUNCIL_NOTICE(HttpStatus.NOT_FOUND, "총동아리연합회 공지사항을 찾을 수 없습니다."), NOT_FOUND_USER(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."), + NOT_FOUND_SCHEDULE(HttpStatus.NOT_FOUND, "일정을 찾을 수 없습니다."), NOT_FOUND_UNREGISTERED_USER(HttpStatus.NOT_FOUND, "임시 유저를 찾을 수 없습니다."), UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다."), NOT_FOUND_CLUB_APPLY_QUESTION(HttpStatus.NOT_FOUND, "동아리 가입 문항을 찾을 수 없습니다."), From ed20f00c5c59aa58a196ee8e299163c60674b17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 16:57:25 +0900 Subject: [PATCH 04/15] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/controller/AdminScheduleController.java | 1 - .../konect/global/auth/interceptor/AuthorizationInterceptor.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java index 096b5675..9edec544 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java @@ -14,7 +14,6 @@ @RequestMapping("/admin/schedule") public class AdminScheduleController implements AdminScheduleApi { - private final AdminScheduleService adminScheduleService; @Override diff --git a/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java b/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java index 088def87..0561585f 100644 --- a/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java +++ b/src/main/java/gg/agit/konect/global/auth/interceptor/AuthorizationInterceptor.java @@ -53,6 +53,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons private Auth findAuthAnnotation(HandlerMethod handlerMethod) { Auth methodAnnotation = handlerMethod.getMethodAnnotation(Auth.class); + if (methodAnnotation != null) { return methodAnnotation; } From 0c3ccffec942bf4d68b071753c420994dff1bbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 16:59:47 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EB=AA=85=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/controller/AdminScheduleApi.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java index 7130a2c7..ae37b1e9 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java @@ -1,10 +1,12 @@ package gg.agit.konect.admin.schedule.controller; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PathVariable; import gg.agit.konect.admin.schedule.dto.AdminScheduleCreateRequest; import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertRequest; @@ -36,4 +38,12 @@ ResponseEntity upsertSchedules( @Valid @RequestBody AdminScheduleUpsertRequest request, @UserId Integer userId ); + + @Operation(summary = "일정을 삭제한다.") + @DeleteMapping("/{scheduleId}") + @Auth(roles = {UserRole.ADMIN}) + ResponseEntity deleteSchedule( + @PathVariable Integer scheduleId, + @UserId Integer userId + ); } From 52a5fa6c55fe673e1ee154800787ad0c0638cd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:00:05 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminScheduleController.java | 7 +++++++ .../schedule/service/AdminScheduleService.java | 14 ++++++++++++++ .../schedule/repository/ScheduleRepository.java | 2 ++ .../repository/UniversityScheduleRepository.java | 2 ++ 4 files changed, 25 insertions(+) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java index 9edec544..a484e9a8 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java @@ -29,4 +29,11 @@ public ResponseEntity upsertSchedules(AdminScheduleUpsertRequest request, return ResponseEntity.ok().build(); } + + @Override + public ResponseEntity deleteSchedule(Integer scheduleId, Integer userId) { + adminScheduleService.deleteSchedule(scheduleId, userId); + + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java index d213ade9..2c9fce6f 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java +++ b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java @@ -71,6 +71,20 @@ public void upsertSchedules(AdminScheduleUpsertRequest request, Integer userId) } } + @Transactional + public void deleteSchedule(Integer scheduleId, Integer userId) { + User user = userRepository.getById(userId); + University university = user.getUniversity(); + + UniversitySchedule universitySchedule = universityScheduleRepository.getByIdAndUniversityId( + scheduleId, + university.getId() + ); + + universityScheduleRepository.delete(universitySchedule); + scheduleRepository.delete(universitySchedule.getSchedule()); + } + private void createUniversitySchedule(AdminScheduleUpsertItemRequest item, University university) { Schedule schedule = Schedule.of( item.title(), diff --git a/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java b/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java index b7b29ed7..c65234ad 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java +++ b/src/main/java/gg/agit/konect/domain/schedule/repository/ScheduleRepository.java @@ -17,6 +17,8 @@ public interface ScheduleRepository extends Repository { Schedule save(Schedule schedule); + void delete(Schedule schedule); + @Query(""" SELECT s FROM Schedule s diff --git a/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java b/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java index d97e7a60..cae4d74b 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java +++ b/src/main/java/gg/agit/konect/domain/schedule/repository/UniversityScheduleRepository.java @@ -14,6 +14,8 @@ public interface UniversityScheduleRepository extends Repository Date: Fri, 23 Jan 2026 17:11:58 +0900 Subject: [PATCH 07/15] =?UTF-8?q?fix:=20=EB=AA=85=EC=84=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/controller/AdminScheduleApi.java | 14 +++++++++++++- .../schedule/dto/AdminScheduleCreateRequest.java | 5 +++-- .../dto/AdminScheduleUpsertItemRequest.java | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java index ae37b1e9..4d9a486d 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java @@ -21,7 +21,13 @@ @RequestMapping("/admin/schedule") public interface AdminScheduleApi { - @Operation(summary = "일정을 생성한다.") + @Operation(summary = "일정을 생성한다.", description = """ + **scheduleType (일정 구분):** + - `UNIVERSITY`: 대학교 일정 + - `CLUB`: 동아리 일정 + - `COUNCIL`: 총동아리연합회 일정 + - `DORM`: 기숙사 일정 + """) @PostMapping @Auth(roles = {UserRole.ADMIN}) ResponseEntity createSchedule( @@ -31,6 +37,12 @@ ResponseEntity createSchedule( @Operation(summary = "일정을 일괄 생성/수정한다.", description = """ scheduleId가 없으면 신규 생성, 있으면 해당 일정 수정입니다. + + **scheduleType (일정 구분):** + - `UNIVERSITY`: 대학교 일정 + - `CLUB`: 동아리 일정 + - `COUNCIL`: 총동아리연합회 일정 + - `DORM`: 기숙사 일정 """) @PutMapping("/batch") @Auth(roles = {UserRole.ADMIN}) diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java index 842d25f1..82e81d0a 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java @@ -17,15 +17,16 @@ public record AdminScheduleCreateRequest( String title, @NotNull - @Schema(description = "일정 시작 일시", example = "2025-12-22 00:00:00", requiredMode = REQUIRED) + @Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime startedAt, @NotNull - @Schema(description = "일정 종료 일시", example = "2026-02-27 23:59:59", requiredMode = REQUIRED) + @Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime endedAt, + @NotNull @Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED) ScheduleType scheduleType ) { diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java index d1d05a92..43fc7ab5 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java @@ -20,12 +20,12 @@ public record AdminScheduleUpsertItemRequest( String title, @NotNull - @Schema(description = "일정 시작 일시", example = "2025-12-22 00:00:00", requiredMode = REQUIRED) + @Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime startedAt, @NotNull - @Schema(description = "일정 종료 일시", example = "2026-02-27 23:59:59", requiredMode = REQUIRED) + @Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime endedAt, From 281c089685418b07a6dec0342b00b83d2fbf54d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:15:45 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=8A=A4=ED=8E=99=20Public=20/=20Admin=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/global/config/SwaggerConfig.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/gg/agit/konect/global/config/SwaggerConfig.java b/src/main/java/gg/agit/konect/global/config/SwaggerConfig.java index e5a5a69e..9c32f24b 100644 --- a/src/main/java/gg/agit/konect/global/config/SwaggerConfig.java +++ b/src/main/java/gg/agit/konect/global/config/SwaggerConfig.java @@ -6,6 +6,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springdoc.core.models.GroupedOpenApi; + import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; @@ -32,6 +34,23 @@ public OpenAPI openAPI() { .addServersItem(server); } + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("Public API") + .pathsToMatch("/**") + .pathsToExclude("/admin/**") + .build(); + } + + @Bean + public GroupedOpenApi adminApi() { + return GroupedOpenApi.builder() + .group("Admin API") + .pathsToMatch("/admin/**") + .build(); + } + private Info apiInfo() { return new Info() .title("KONECT API") From 3acdd06de8e06052363daa6ec10cbbd4d1fe66e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:19:31 +0900 Subject: [PATCH 09/15] =?UTF-8?q?fix:=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/admin/schedule/controller/AdminScheduleApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java index 4d9a486d..df20dd08 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java @@ -18,7 +18,7 @@ import jakarta.validation.Valid; @Tag(name = "(Admin) Schedule: 일정", description = "어드민 일정 API") -@RequestMapping("/admin/schedule") +@RequestMapping("/admin/schedules") public interface AdminScheduleApi { @Operation(summary = "일정을 생성한다.", description = """ From 3aa189dda6f357f0662c84e1c61ac519913bee85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:37:35 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91,=20=EC=A2=85=EB=A3=8C=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/schedule/model/Schedule.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java b/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java index d1c4b6ba..c35a0a06 100644 --- a/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java +++ b/src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java @@ -9,6 +9,8 @@ import java.time.temporal.ChronoUnit; import gg.agit.konect.global.model.BaseEntity; +import gg.agit.konect.global.code.ApiResponseCode; +import gg.agit.konect.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Enumerated; @@ -61,6 +63,8 @@ private Schedule( this.startedAt = startedAt; this.endedAt = endedAt; this.scheduleType = scheduleType; + + validateDateTimeRange(startedAt, endedAt); } public static Schedule of( @@ -83,12 +87,24 @@ public void update( LocalDateTime endedAt, ScheduleType scheduleType ) { + validateDateTimeRange(startedAt, endedAt); + this.title = title; this.startedAt = startedAt; this.endedAt = endedAt; this.scheduleType = scheduleType; } + private void validateDateTimeRange(LocalDateTime startedAt, LocalDateTime endedAt) { + if (startedAt == null || endedAt == null) { + return; + } + + if (startedAt.isAfter(endedAt)) { + throw CustomException.of(ApiResponseCode.INVALID_DATE_TIME); + } + } + public Integer calculateDDay(LocalDate today) { if (today.isBefore(this.startedAt.toLocalDate())) { return (int)ChronoUnit.DAYS.between(today, this.startedAt.toLocalDate()); From 0edd36383e004bfd95f87af0f8c0161915077988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:46:16 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20DTO=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/dto/AdminScheduleCreateRequest.java | 8 ++++---- .../schedule/dto/AdminScheduleUpsertItemRequest.java | 8 ++++---- .../admin/schedule/dto/AdminScheduleUpsertRequest.java | 7 ++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java index 82e81d0a..cbb37449 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleCreateRequest.java @@ -12,21 +12,21 @@ import jakarta.validation.constraints.NotNull; public record AdminScheduleCreateRequest( - @NotBlank + @NotBlank(message = "일정 제목은 필수 입력입니다.") @Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED) String title, - @NotNull + @NotNull(message = "일정 시작 일시는 필수 입력입니다.") @Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime startedAt, - @NotNull + @NotNull(message = "일정 종료 일시는 필수 입력입니다.") @Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime endedAt, - @NotNull + @NotNull(message = "일정 종류는 필수 입력입니다.") @Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED) ScheduleType scheduleType ) { diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java index 43fc7ab5..75af01a1 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertItemRequest.java @@ -15,21 +15,21 @@ public record AdminScheduleUpsertItemRequest( @Schema(description = "수정할 일정 ID (없으면 신규 생성)", example = "1") Integer scheduleId, - @NotBlank + @NotBlank(message = "일정 제목은 필수 입력입니다.") @Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED) String title, - @NotNull + @NotNull(message = "일정 시작 일시는 필수 입력입니다.") @Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime startedAt, - @NotNull + @NotNull(message = "일정 종료 일시는 필수 입력입니다.") @Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss") LocalDateTime endedAt, - @NotNull + @NotNull(message = "일정 종류는 필수 입력입니다.") @Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED) ScheduleType scheduleType ) { diff --git a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java index e517fe4c..e9e4191f 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java +++ b/src/main/java/gg/agit/konect/admin/schedule/dto/AdminScheduleUpsertRequest.java @@ -7,13 +7,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; public record AdminScheduleUpsertRequest( - @NotNull - @NotEmpty - @Valid + @NotEmpty(message = "일정 목록은 필수 입력입니다.") @Schema(description = "생성/수정할 일정 목록", requiredMode = REQUIRED) - List schedules + List<@Valid AdminScheduleUpsertItemRequest> schedules ) { } From 2927757888967713bb510dff99f4e2245e1ce33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:57:47 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=20=EC=9D=BC=EC=A0=95=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminScheduleService.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java index 2c9fce6f..f4b77e32 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java +++ b/src/main/java/gg/agit/konect/admin/schedule/service/AdminScheduleService.java @@ -1,5 +1,7 @@ package gg.agit.konect.admin.schedule.service; +import java.time.LocalDateTime; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -7,6 +9,7 @@ import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertItemRequest; import gg.agit.konect.admin.schedule.dto.AdminScheduleUpsertRequest; import gg.agit.konect.domain.schedule.model.Schedule; +import gg.agit.konect.domain.schedule.model.ScheduleType; import gg.agit.konect.domain.schedule.model.UniversitySchedule; import gg.agit.konect.domain.schedule.repository.ScheduleRepository; import gg.agit.konect.domain.schedule.repository.UniversityScheduleRepository; @@ -29,21 +32,13 @@ public void createSchedule(AdminScheduleCreateRequest request, Integer userId) { User user = userRepository.getById(userId); University university = user.getUniversity(); - Schedule schedule = Schedule.of( + createUniversitySchedule( + university, request.title(), request.startedAt(), request.endedAt(), request.scheduleType() ); - - Schedule savedSchedule = scheduleRepository.save(schedule); - - UniversitySchedule universitySchedule = UniversitySchedule.of( - savedSchedule, - university - ); - - universityScheduleRepository.save(universitySchedule); } @Transactional @@ -53,7 +48,13 @@ public void upsertSchedules(AdminScheduleUpsertRequest request, Integer userId) for (AdminScheduleUpsertItemRequest item : request.schedules()) { if (item.scheduleId() == null) { - createUniversitySchedule(item, university); + createUniversitySchedule( + university, + item.title(), + item.startedAt(), + item.endedAt(), + item.scheduleType() + ); continue; } @@ -85,12 +86,18 @@ public void deleteSchedule(Integer scheduleId, Integer userId) { scheduleRepository.delete(universitySchedule.getSchedule()); } - private void createUniversitySchedule(AdminScheduleUpsertItemRequest item, University university) { + private void createUniversitySchedule( + University university, + String title, + LocalDateTime startedAt, + LocalDateTime endedAt, + ScheduleType scheduleType + ) { Schedule schedule = Schedule.of( - item.title(), - item.startedAt(), - item.endedAt(), - item.scheduleType() + title, + startedAt, + endedAt, + scheduleType ); Schedule savedSchedule = scheduleRepository.save(schedule); From da98a0dacb12c0ed5e66e90141793f0dbac674a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 17:58:53 +0900 Subject: [PATCH 13/15] =?UTF-8?q?fix:=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/schedule/controller/AdminScheduleController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java index a484e9a8..6c9269a7 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleController.java @@ -11,7 +11,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/admin/schedule") +@RequestMapping("/admin/schedules") public class AdminScheduleController implements AdminScheduleApi { private final AdminScheduleService adminScheduleService; From 3351fa5afdb1650c5ca46243f9ad128449b99042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 18:00:28 +0900 Subject: [PATCH 14/15] =?UTF-8?q?fix:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=EC=97=90=EC=84=9C=EB=8F=84=20@Auth=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gg/agit/konect/global/auth/annotation/Auth.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java b/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java index 082ac212..d0f15727 100644 --- a/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java +++ b/src/main/java/gg/agit/konect/global/auth/annotation/Auth.java @@ -1,6 +1,7 @@ package gg.agit.konect.global.auth.annotation; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; @@ -8,7 +9,7 @@ import gg.agit.konect.domain.user.enums.UserRole; -@Target(METHOD) +@Target({TYPE, METHOD}) @Retention(RUNTIME) public @interface Auth { From d004db9e4894e986639e7f4db28aaa472e27c659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 23 Jan 2026 18:37:44 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/admin/schedule/controller/AdminScheduleApi.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java index df20dd08..7fbff8e6 100644 --- a/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java +++ b/src/main/java/gg/agit/konect/admin/schedule/controller/AdminScheduleApi.java @@ -19,6 +19,7 @@ @Tag(name = "(Admin) Schedule: 일정", description = "어드민 일정 API") @RequestMapping("/admin/schedules") +@Auth(roles = {UserRole.ADMIN}) public interface AdminScheduleApi { @Operation(summary = "일정을 생성한다.", description = """ @@ -29,7 +30,6 @@ public interface AdminScheduleApi { - `DORM`: 기숙사 일정 """) @PostMapping - @Auth(roles = {UserRole.ADMIN}) ResponseEntity createSchedule( @Valid @RequestBody AdminScheduleCreateRequest request, @UserId Integer userId @@ -45,7 +45,6 @@ ResponseEntity createSchedule( - `DORM`: 기숙사 일정 """) @PutMapping("/batch") - @Auth(roles = {UserRole.ADMIN}) ResponseEntity upsertSchedules( @Valid @RequestBody AdminScheduleUpsertRequest request, @UserId Integer userId @@ -53,7 +52,6 @@ ResponseEntity upsertSchedules( @Operation(summary = "일정을 삭제한다.") @DeleteMapping("/{scheduleId}") - @Auth(roles = {UserRole.ADMIN}) ResponseEntity deleteSchedule( @PathVariable Integer scheduleId, @UserId Integer userId