Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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;
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/schedules")
@Auth(roles = {UserRole.ADMIN})
public interface AdminScheduleApi {

@Operation(summary = "일정을 생성한다.", description = """
**scheduleType (일정 구분):**
- `UNIVERSITY`: 대학교 일정
- `CLUB`: 동아리 일정
- `COUNCIL`: 총동아리연합회 일정
- `DORM`: 기숙사 일정
""")
@PostMapping
ResponseEntity<Void> createSchedule(
@Valid @RequestBody AdminScheduleCreateRequest request,
@UserId Integer userId
);

@Operation(summary = "일정을 일괄 생성/수정한다.", description = """
scheduleId가 없으면 신규 생성, 있으면 해당 일정 수정입니다.

**scheduleType (일정 구분):**
- `UNIVERSITY`: 대학교 일정
- `CLUB`: 동아리 일정
- `COUNCIL`: 총동아리연합회 일정
- `DORM`: 기숙사 일정
""")
@PutMapping("/batch")
ResponseEntity<Void> upsertSchedules(
@Valid @RequestBody AdminScheduleUpsertRequest request,
@UserId Integer userId
);

@Operation(summary = "일정을 삭제한다.")
@DeleteMapping("/{scheduleId}")
ResponseEntity<Void> deleteSchedule(
@PathVariable Integer scheduleId,
@UserId Integer userId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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/schedules")
public class AdminScheduleController implements AdminScheduleApi {

private final AdminScheduleService adminScheduleService;

@Override
public ResponseEntity<Void> createSchedule(AdminScheduleCreateRequest request, Integer userId) {
adminScheduleService.createSchedule(request, userId);

return ResponseEntity.ok().build();
}

@Override
public ResponseEntity<Void> upsertSchedules(AdminScheduleUpsertRequest request, Integer userId) {
adminScheduleService.upsertSchedules(request, userId);

return ResponseEntity.ok().build();
}

@Override
public ResponseEntity<Void> deleteSchedule(Integer scheduleId, Integer userId) {
adminScheduleService.deleteSchedule(scheduleId, userId);

return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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(message = "일정 제목은 필수 입력입니다.")
@Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED)
String title,

@NotNull(message = "일정 시작 일시는 필수 입력입니다.")
@Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss")
LocalDateTime startedAt,

@NotNull(message = "일정 종료 일시는 필수 입력입니다.")
@Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss")
LocalDateTime endedAt,

@NotNull(message = "일정 종류는 필수 입력입니다.")
@Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED)
ScheduleType scheduleType
) {
}
Original file line number Diff line number Diff line change
@@ -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(message = "일정 제목은 필수 입력입니다.")
@Schema(description = "일정 제목", example = "동계방학", requiredMode = REQUIRED)
String title,

@NotNull(message = "일정 시작 일시는 필수 입력입니다.")
@Schema(description = "일정 시작 일시", example = "2025.12.22 00:00:00", requiredMode = REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss")
LocalDateTime startedAt,

@NotNull(message = "일정 종료 일시는 필수 입력입니다.")
@Schema(description = "일정 종료 일시", example = "2026.02.27 23:59:59", requiredMode = REQUIRED)
@JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss")
LocalDateTime endedAt,

@NotNull(message = "일정 종류는 필수 입력입니다.")
@Schema(description = "일정 종류", example = "UNIVERSITY", requiredMode = REQUIRED)
ScheduleType scheduleType
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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;

public record AdminScheduleUpsertRequest(
@NotEmpty(message = "일정 목록은 필수 입력입니다.")
@Schema(description = "생성/수정할 일정 목록", requiredMode = REQUIRED)
List<@Valid AdminScheduleUpsertItemRequest> schedules
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package gg.agit.konect.admin.schedule.service;

import java.time.LocalDateTime;

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.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;
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();

createUniversitySchedule(
university,
request.title(),
request.startedAt(),
request.endedAt(),
request.scheduleType()
);
}

@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(
university,
item.title(),
item.startedAt(),
item.endedAt(),
item.scheduleType()
);
continue;
}

UniversitySchedule universitySchedule = universityScheduleRepository.getByIdAndUniversityId(
item.scheduleId(),
university.getId()
);

universitySchedule.getSchedule().update(
item.title(),
item.startedAt(),
item.endedAt(),
item.scheduleType()
);
}
}

@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());
Comment on lines +85 to +86
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일정을 삭제할 때 UniversitySchedule만 삭제하고 Schedule도 함께 삭제하고 있습니다. 그러나 Schedule은 다른 타입(CLUB, COUNCIL, DORM)의 일정과도 연결될 수 있는데, 현재 구조에서는 UniversitySchedule이 삭제되면 Schedule도 무조건 삭제됩니다. 만약 하나의 Schedule이 여러 엔티티(예: ClubSchedule, CouncilSchedule 등)와 연결될 수 있다면, 다른 곳에서 참조하고 있는 Schedule을 삭제하여 데이터 무결성 문제가 발생할 수 있습니다. Schedule과 UniversitySchedule의 관계가 1:1이 확실하다면 문제없지만, 아키텍처를 확인해야 합니다.

Copilot uses AI. Check for mistakes.
}

private void createUniversitySchedule(
University university,
String title,
LocalDateTime startedAt,
LocalDateTime endedAt,
ScheduleType scheduleType
) {
Schedule schedule = Schedule.of(
title,
startedAt,
endedAt,
scheduleType
);

Schedule savedSchedule = scheduleRepository.save(schedule);

UniversitySchedule universitySchedule = UniversitySchedule.of(
savedSchedule,
university
);

universityScheduleRepository.save(universitySchedule);
}
}
42 changes: 42 additions & 0 deletions src/main/java/gg/agit/konect/domain/schedule/model/Schedule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +63,46 @@ private Schedule(
this.startedAt = startedAt;
this.endedAt = endedAt;
this.scheduleType = scheduleType;

validateDateTimeRange(startedAt, endedAt);
}

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
) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,12 @@

public interface ScheduleRepository extends Repository<Schedule, Integer> {

Optional<Schedule> findById(Integer id);

Schedule save(Schedule schedule);

void delete(Schedule schedule);

@Query("""
SELECT s
FROM Schedule s
Expand Down
Loading