Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum DomainType {
ABTEST("AB 테스트"),

LANDS("복덕방"),
LECTURES("강의"),
COOP_SHOPS("생협 매장"),

USERS("회원"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package in.koreatech.koin.admin.lecture.controller;

import static in.koreatech.koin.admin.history.enums.DomainType.LECTURES;
import static in.koreatech.koin.domain.user.model.UserType.ADMIN;
import static in.koreatech.koin.global.code.ApiResponseCode.DUPLICATE_LECTURE;
import static in.koreatech.koin.global.code.ApiResponseCode.FORBIDDEN_ADMIN;
import static in.koreatech.koin.global.code.ApiResponseCode.INVALID_REQUEST_BODY;
import static in.koreatech.koin.global.code.ApiResponseCode.NOT_FOUND_SEMESTER;
import static in.koreatech.koin.global.code.ApiResponseCode.NOT_READABLE_HTTP_MESSAGE;
import static in.koreatech.koin.global.code.ApiResponseCode.OK;
import static in.koreatech.koin.global.code.ApiResponseCode.UNAUTHORIZED_USER;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import in.koreatech.koin.admin.history.aop.AdminActivityLogging;
import in.koreatech.koin.admin.lecture.dto.AdminLectureCreateRequest;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.code.ApiResponseCodes;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

@Tag(name = "(Admin) Lecture: 강의", description = "관리자 권한으로 강의를 관리한다")
public interface AdminLectureApi {

@ApiResponseCodes({
OK,
INVALID_REQUEST_BODY,
UNAUTHORIZED_USER,
FORBIDDEN_ADMIN,
NOT_READABLE_HTTP_MESSAGE,
NOT_FOUND_SEMESTER,
DUPLICATE_LECTURE
})
@Operation(
summary = "강의를 일괄 생성한다",
description = """
## 강의 일괄 생성
입력한 연도와 학기에 해당하는 강의들을 한 번에 생성합니다.
요청한 학기가 존재하지 않거나 중복 강의가 포함된 경우 강의를 생성하지 않습니다.
중복 여부는 학기, 과목 코드, 분반의 조합을 기준으로 판단합니다.

## 요청 Body 필드 설명
- `year`: 강의를 등록할 연도 (필수, 양수)
- `term`: 강의를 등록할 학기 (필수)
- `FIRST`: 1학기
- `SECOND`: 2학기
- `SUMMER`: 여름학기
- `WINTER`: 겨울학기
- `lectures`: 생성할 강의 정보 리스트 (필수, 빈 리스트 불가)
- `code`: 과목 코드 (필수, 최대 10자)
- `name`: 과목 이름 (필수, 최대 50자)
- `grades`: 대상 학년 (필수, 최대 2자)
- `lecture_class`: 분반 (필수, 최대 3자)
- `regular_number`: 수강 인원 (필수, 최대 4자)
- `department`: 학부 (필수, 최대 30자)
- `target`: 수강 대상 (필수, 최대 200자)
- `professor`: 교수명 (선택, 최대 30자)
- `is_english`: 영어 강의 여부 (필수, 최대 2자)
- `design_score`: 설계 학점 (필수, 최대 2자)
- `is_elearning`: 이러닝 여부 (필수, 최대 2자)
- `class_time`: 강의 시간 코드 리스트 (필수, 최대 50개, 각 값은 0~999)

## 처리 결과
- 모든 강의가 유효한 경우 일괄 생성하고 `200 OK`를 반환합니다.
- 요청 리스트 내부 또는 기존 강의와 중복되는 항목이 있으면 `DUPLICATE_LECTURE`를 반환합니다.
"""
)
@SecurityRequirement(name = "Jwt Authentication")
@PostMapping("/admin/lectures")
@AdminActivityLogging(domain = LECTURES)
ResponseEntity<Void> createLectures(
@RequestBody @Valid AdminLectureCreateRequest request,
@Auth(permit = {ADMIN}) Integer adminId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package in.koreatech.koin.admin.lecture.controller;

import static in.koreatech.koin.admin.history.enums.DomainType.LECTURES;
import static in.koreatech.koin.domain.user.model.UserType.ADMIN;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.admin.history.aop.AdminActivityLogging;
import in.koreatech.koin.admin.lecture.dto.AdminLectureCreateRequest;
import in.koreatech.koin.admin.lecture.service.AdminLectureService;
import in.koreatech.koin.global.auth.Auth;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AdminLectureController implements AdminLectureApi {

private final AdminLectureService adminLectureService;

@PostMapping("/admin/lectures")
@AdminActivityLogging(domain = LECTURES)
public ResponseEntity<Void> createLectures(
@RequestBody @Valid AdminLectureCreateRequest request,
@Auth(permit = {ADMIN}) Integer adminId
) {
adminLectureService.createLectures(request);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package in.koreatech.koin.admin.lecture.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.util.List;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.domain.timetable.model.Lecture;
import in.koreatech.koin.domain.timetableV3.model.Term;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;

@JsonNaming(SnakeCaseStrategy.class)
public record AdminLectureCreateRequest(
@Schema(description = "연도", example = "2026", requiredMode = REQUIRED)
@NotNull(message = "연도는 필수입니다.")
@Positive(message = "연도는 양수여야 합니다.")
Integer year,

@Schema(description = "학기", example = "FIRST", requiredMode = REQUIRED)
@NotNull(message = "학기는 필수입니다.")
Term term,

@Schema(description = "강의 정보 리스트", requiredMode = REQUIRED)
@Valid
@NotEmpty(message = "강의 정보 리스트는 비어 있을 수 없습니다.")
List<LectureRequest> lectures
) {

public List<Lecture> toEntities(String semester) {
return lectures.stream()
.map(lecture -> lecture.toEntity(semester))
.toList();
}

@JsonNaming(SnakeCaseStrategy.class)
public record LectureRequest(
@Schema(description = "과목 코드", example = "ARB244", requiredMode = REQUIRED)
@NotBlank(message = "과목 코드는 필수입니다.")
@Size(max = 10, message = "과목 코드는 10자 이하여야 합니다.")
String code,

@Schema(description = "과목 이름", example = "건축구조의 이해 및 실습", requiredMode = REQUIRED)
@NotBlank(message = "과목 이름은 필수입니다.")
@Size(max = 50, message = "과목 이름은 50자 이하여야 합니다.")
String name,

@Schema(description = "대상 학년", example = "3", requiredMode = REQUIRED)
@NotBlank(message = "대상 학년은 필수입니다.")
@Size(max = 2, message = "대상 학년은 2자 이하여야 합니다.")
String grades,

@Schema(description = "분반", example = "01", requiredMode = REQUIRED)
@NotBlank(message = "분반은 필수입니다.")
@Size(max = 3, message = "분반은 3자 이하여야 합니다.")
String lectureClass,

@Schema(description = "수강 인원", example = "25", requiredMode = REQUIRED)
@NotNull(message = "수강 인원은 필수입니다.")
@Size(max = 4, message = "수강 인원은 4자 이하여야 합니다.")
String regularNumber,

@Schema(description = "학부", example = "디자인ㆍ건축공학부", requiredMode = REQUIRED)
@NotBlank(message = "학부는 필수입니다.")
@Size(max = 30, message = "학부는 30자 이하여야 합니다.")
String department,

@Schema(description = "대상", example = "디자 1 건축", requiredMode = REQUIRED)
@NotNull(message = "대상은 필수입니다.")
@Size(max = 200, message = "대상은 200자 이하여야 합니다.")
String target,

@Schema(description = "교수", example = "황현식", requiredMode = NOT_REQUIRED)
@Size(max = 30, message = "교수명은 30자 이하여야 합니다.")
String professor,

@Schema(description = "영어 강의 여부", example = "N", requiredMode = REQUIRED)
@NotNull(message = "영어 강의 여부는 필수입니다.")
@Size(max = 2, message = "영어 강의 여부는 2자 이하여야 합니다.")
String isEnglish,

@Schema(description = "설계 학점", example = "0", requiredMode = REQUIRED)
@NotBlank(message = "설계 학점은 필수입니다.")
@Size(max = 2, message = "설계 학점은 2자 이하여야 합니다.")
String designScore,

@Schema(description = "이러닝 여부", example = "N", requiredMode = REQUIRED)
@NotNull(message = "이러닝 여부는 필수입니다.")
@Size(max = 2, message = "이러닝 여부는 2자 이하여야 합니다.")
String isElearning,
Comment on lines +68 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject blank values for required lecture fields.

@NotNull accepts "" and whitespace-only values. @Size(max = ...) does not reject them. The API can save blank values for regularNumber, target, isEnglish, and isElearning although these fields are required.

Use @NotBlank for required text fields. Use an allowed-value constraint for isEnglish and isElearning if the accepted values are fixed.

Proposed fix
-        `@NotNull`(message = "수강 인원은 필수입니다.")
+        `@NotBlank`(message = "수강 인원은 필수입니다.")
         `@Size`(max = 4, message = "수강 인원은 4자 이하여야 합니다.")
         String regularNumber,
...
-        `@NotNull`(message = "대상은 필수입니다.")
+        `@NotBlank`(message = "대상은 필수입니다.")
         `@Size`(max = 200, message = "대상은 200자 이하여야 합니다.")
         String target,
...
-        `@NotNull`(message = "영어 강의 여부는 필수입니다.")
+        `@NotBlank`(message = "영어 강의 여부는 필수입니다.")
         `@Size`(max = 2, message = "영어 강의 여부는 2자 이하여야 합니다.")
         String isEnglish,
...
-        `@NotNull`(message = "이러닝 여부는 필수입니다.")
+        `@NotBlank`(message = "이러닝 여부는 필수입니다.")
         `@Size`(max = 2, message = "이러닝 여부는 2자 이하여야 합니다.")
         String isElearning,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Schema(description = "수강 인원", example = "25", requiredMode = REQUIRED)
@NotNull(message = "수강 인원은 필수입니다.")
@Size(max = 4, message = "수강 인원은 4자 이하여야 합니다.")
String regularNumber,
@Schema(description = "학부", example = "디자인ㆍ건축공학부", requiredMode = REQUIRED)
@NotBlank(message = "학부는 필수입니다.")
@Size(max = 30, message = "학부는 30자 이하여야 합니다.")
String department,
@Schema(description = "대상", example = "디자 1 건축", requiredMode = REQUIRED)
@NotNull(message = "대상은 필수입니다.")
@Size(max = 200, message = "대상은 200자 이하여야 합니다.")
String target,
@Schema(description = "교수", example = "황현식", requiredMode = NOT_REQUIRED)
@Size(max = 30, message = "교수명은 30자 이하여야 합니다.")
String professor,
@Schema(description = "영어 강의 여부", example = "N", requiredMode = REQUIRED)
@NotNull(message = "영어 강의 여부는 필수입니다.")
@Size(max = 2, message = "영어 강의 여부는 2자 이하여야 합니다.")
String isEnglish,
@Schema(description = "설계 학점", example = "0", requiredMode = REQUIRED)
@NotBlank(message = "설계 학점은 필수입니다.")
@Size(max = 2, message = "설계 학점은 2자 이하여야 합니다.")
String designScore,
@Schema(description = "이러닝 여부", example = "N", requiredMode = REQUIRED)
@NotNull(message = "이러닝 여부는 필수입니다.")
@Size(max = 2, message = "이러닝 여부는 2자 이하여야 합니다.")
String isElearning,
`@Schema`(description = "수강 인원", example = "25", requiredMode = REQUIRED)
`@NotBlank`(message = "수강 인원은 필수입니다.")
`@Size`(max = 4, message = "수강 인원은 4자 이하여야 합니다.")
String regularNumber,
`@Schema`(description = "학부", example = "디자인ㆍ건축공학부", requiredMode = REQUIRED)
`@NotBlank`(message = "학부는 필수입니다.")
`@Size`(max = 30, message = "학부는 30자 이하여야 합니다.")
String department,
`@Schema`(description = "대상", example = "디자 1 건축", requiredMode = REQUIRED)
`@NotBlank`(message = "대상은 필수입니다.")
`@Size`(max = 200, message = "대상은 200자 이하여야 합니다.")
String target,
`@Schema`(description = "교수", example = "황현식", requiredMode = NOT_REQUIRED)
`@Size`(max = 30, message = "교수명은 30자 이하여야 합니다.")
String professor,
`@Schema`(description = "영어 강의 여부", example = "N", requiredMode = REQUIRED)
`@NotBlank`(message = "영어 강의 여부는 필수입니다.")
`@Size`(max = 2, message = "영어 강의 여부는 2자 이하여야 합니다.")
String isEnglish,
`@Schema`(description = "설계 학점", example = "0", requiredMode = REQUIRED)
`@NotBlank`(message = "설계 학점은 필수입니다.")
`@Size`(max = 2, message = "설계 학점은 2자 이하여야 합니다.")
String designScore,
`@Schema`(description = "이러닝 여부", example = "N", requiredMode = REQUIRED)
`@NotBlank`(message = "이러닝 여부는 필수입니다.")
`@Size`(max = 2, message = "이러닝 여부는 2자 이하여야 합니다.")
String isElearning,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/in/koreatech/koin/admin/lecture/dto/AdminLectureCreateRequest.java`
around lines 68 - 100, Update the required text fields regularNumber, target,
isEnglish, and isElearning in AdminLectureCreateRequest to reject empty and
whitespace-only input by replacing `@NotNull` with `@NotBlank`. For isEnglish and
isElearning, also apply the existing or appropriate allowed-value constraint if
their valid values are fixed, while preserving the current size limits and
validation messages.


@Schema(description = "강의 시간", example = "[200, 201, 202, 203]", requiredMode = REQUIRED)
@NotNull(message = "강의 시간은 필수입니다.")
@Size(max = 50, message = "강의 시간은 최대 50개까지 입력할 수 있습니다.")
List<@NotNull @PositiveOrZero @Max(999) Integer> classTime
) {

public Lecture toEntity(String semester) {
return Lecture.builder()
.semester(semester)
.code(code)
.name(name)
.grades(grades)
.lectureClass(lectureClass)
.regularNumber(regularNumber)
.department(department)
.target(target)
.professor(professor)
.isEnglish(isEnglish)
.designScore(designScore)
.isElearning(isElearning)
.classTime(classTime.toString())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package in.koreatech.koin.admin.lecture.model;

public record LectureKey(
String code,
String lectureClass
) {

public static LectureKey of(String code, String lectureClass) {
return new LectureKey(code, lectureClass);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package in.koreatech.koin.admin.lecture.repository;

import java.util.List;

import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.timetable.model.Lecture;

public interface AdminLectureRepository extends Repository<Lecture, Integer> {

boolean existsBySemesterAndCodeAndLectureClass(String semester, String code, String lectureClass);

List<Lecture> saveAll(Iterable<Lecture> lectures);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package in.koreatech.koin.admin.lecture.repository;

import static in.koreatech.koin.global.code.ApiResponseCode.NOT_FOUND_SEMESTER;

import java.util.Optional;

import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.timetable.model.Semester;
import in.koreatech.koin.domain.timetableV3.model.Term;
import in.koreatech.koin.global.exception.CustomException;

public interface AdminSemesterRepository extends Repository<Semester, Integer> {

Optional<Semester> findByYearAndTerm(Integer year, Term term);

default Semester getByYearAndTerm(Integer year, Term term) {
return findByYearAndTerm(year, term)
.orElseThrow(() -> CustomException.of(
NOT_FOUND_SEMESTER,
"year: " + year + ", term: " + term.getDescription()
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package in.koreatech.koin.admin.lecture.service;

import static in.koreatech.koin.global.code.ApiResponseCode.DUPLICATE_LECTURE;

import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import in.koreatech.koin.admin.lecture.dto.AdminLectureCreateRequest;
import in.koreatech.koin.admin.lecture.dto.AdminLectureCreateRequest.LectureRequest;
import in.koreatech.koin.admin.lecture.model.LectureKey;
import in.koreatech.koin.admin.lecture.repository.AdminLectureRepository;
import in.koreatech.koin.admin.lecture.repository.AdminSemesterRepository;
import in.koreatech.koin.domain.timetable.model.Semester;
import in.koreatech.koin.global.exception.CustomException;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AdminLectureService {

private final AdminLectureRepository adminLectureRepository;
private final AdminSemesterRepository adminSemesterRepository;

@Transactional
public void createLectures(AdminLectureCreateRequest request) {
Semester semester = adminSemesterRepository.getByYearAndTerm(request.year(), request.term());

Set<LectureKey> lectureKeys = new HashSet<>();
for (LectureRequest lecture : request.lectures()) {
LectureKey lectureKey = LectureKey.of(lecture.code(), lecture.lectureClass());
if (!lectureKeys.add(lectureKey) || adminLectureRepository.existsBySemesterAndCodeAndLectureClass(
semester.getSemester(), lecture.code(), lecture.lectureClass()
)) {
throw CustomException.of(
DUPLICATE_LECTURE,
"semester: " + semester.getSemester() + ", code: " + lecture.code()
+ ", lectureClass: " + lecture.lectureClass()
);
}
}

adminLectureRepository.saveAll(request.toEntities(semester.getSemester()));
Comment on lines +35 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f 'Lecture.java' src
rg -n -C 4 '`@Table`|`@UniqueConstraint`|uniqueConstraints|semester|lectureClass|lecture_class' src
rg -n -i -C 4 'create table.*lecture|alter table.*lecture|unique.*lecture|lecture_class' .

Repository: BCSDLab/KOIN_API_V2

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- AdminLectureService ---\n'
fd -t f 'AdminLectureService.java' src -x sh -c 'wc -l "$1"; sed -n "1,120p" "$1"' sh {}

printf '\n--- AdminLectureRepository definitions/usages ---\n'
fd -t f 'AdminLectureRepository.java|AdminLecture.*Repository.java' src -x sh -c 'echo "== $1 =="; cat -n "$1"' sh {}

printf '\n--- Lecture model entity ---\n'
fd -t f 'Lecture.java' src -x sh -c 'echo "== $1 =="; cat -n "$1"' sh {}

printf '\n--- targeted DDL/migrations references ---\n'
git ls-files | rg -i '(migration|flyway|liquibase|ddl|schema|lecture)' | head -200 | while read -r f; do
  case "$f" in
    *.java|*.sql|*.yml|*.yaml|*.properties|*.xml)
      if rg -qi 'lectures|uniqueConstraints|semester|code|lecture_class|lectureclass|lecture_class' "$f"; then
        echo "== $f =="
        rg -n -C 3 -i 'lectures|uniqueConstraints| semester_date| code| lecture_class| lectureClass|lectureclass' "$f" | head -80
      fi
      ;;
  esac
done

Repository: BCSDLab/KOIN_API_V2

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- AdminLectureService ---'
fd -t f 'AdminLectureService.java' src -x sh -c 'wc -l "$1"; sed -n "1,140p" "$1"' sh {}

echo
echo '--- AdminLecture repository definitions/usages ---'
fd -t f 'AdminLecture.*Repository.java' src -x sh -c 'echo "==$1=="; cat -n "$1"' sh {} || true

echo
echo '--- lecture repository ---'
fd -t f 'LectureRepository.java' src -x sh -c 'echo "==$1=="; cat -n "$1"' sh {}

echo
echo '--- Lecture model entity ---'
fd -t f 'Lecture.java' src -x sh -c 'echo "==$1=="; cat -n "$1"' sh {}

echo
echo '--- targeted DDL/migrations references ---'
git ls-files | rg -i '(migration|flyway|liquibase|ddl|schema|lecture)' | head -200 | while read -r f; do
  case "$f" in
    *.java|*.sql|*.yml|*.yaml|*.properties|*.xml)
      if rg -qi 'lectures|uniqueConstraints|semester_date| code| lecture_class|lectureClass|lectureclass' "$f"; then
        echo "==$f=="
        rg -n -C 3 -i 'lectures|uniqueConstraints|semester_date| code| lecture_class|lectureClass|lectureclass' "$f" | head -120
      fi
      ;;
  esac
done

Repository: BCSDLab/KOIN_API_V2

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- files likely containing lectures DDL/migrations/config ---'
git ls-files | rg -i 'lectures|lecture|unique|ddl|schema|migration|flyway|liquibase|sql|yml|yaml|properties|pom|gradle' | head -300

echo
echo '--- exact unique/DUPLECTURE references ---'
rg -n -C 3 'DUPLICATE_LECTURE|uniqueConstraints|uniqueConstraints|constraint.*lectures|unique.*lectures|lectures\(|UNIQUE.*semester_date|UNIQUE|lecture_class|LECTURES' src main .github --glob '!build/**' --glob '!dist/**' || true

echo
echo '--- exception mapper around constraint failures ---'
fd -t f 'GlobalExceptionHandler.java|ExceptionHandler.java|*Exception*Handler.java|ApiResponseCode.java|ExceptionMapper.java|DatabaseAdapterResolver.java' src -x sh -c 'echo "==$1=="; sed -n "1,220p" "$1"' sh {} | rg -n -C 4 'DUPLICATE_LECTURE|DataIntegrity|ConstraintViolation|Duplicate|ex\.|constraint|unique|lectures' || true

Repository: BCSDLab/KOIN_API_V2

Length of output: 50375


Add a database guarantee for lecture identity.

Lecture maps lectures(semester_date, code, class) but has no unique constraint, and the current code only does an in-transaction existsBy... before saveAll(). Concurrent requests can both see no row and insert duplicate (semester, code, lectureClass) entries. Add a unique constraint for this business key and handle mapped constraint failures with DUPLICATE_LECTURE.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/in/koreatech/koin/admin/lecture/service/AdminLectureService.java`
around lines 35 - 46, Add a database-level unique constraint on the Lecture
business key (semester_date, code, and class), then update the save flow in
AdminLectureService to catch the resulting mapped constraint violation from
saveAll and translate it to DUPLICATE_LECTURE, preserving the existing duplicate
validation message and behavior.

}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package in.koreatech.koin.domain.timetableV3.model;

import in.koreatech.koin.domain.timetableV3.exception.InvalidTermFormatException;
import static in.koreatech.koin.global.code.ApiResponseCode.INVALID_SEMESTER_FORMAT;

import in.koreatech.koin.global.exception.CustomException;
import lombok.Getter;

@Getter
Expand All @@ -24,6 +26,6 @@ public static Term fromDescription(String description) {
return term;
}
}
throw new InvalidTermFormatException("term : " + description);
throw CustomException.of(INVALID_SEMESTER_FORMAT, "term: " + description);
}
}
Loading
Loading