diff --git a/backend/src/main/java/com/festago/auth/config/LoginConfig.java b/backend/src/main/java/com/festago/auth/config/LoginConfig.java index e340587a6..5d06fa199 100644 --- a/backend/src/main/java/com/festago/auth/config/LoginConfig.java +++ b/backend/src/main/java/com/festago/auth/config/LoginConfig.java @@ -32,7 +32,7 @@ public void addArgumentResolvers(List resolvers) public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(adminAuthInterceptor()) .addPathPatterns("/admin/**", "/js/admin/**") - .excludePathPatterns("/admin/login", "/admin/initialize"); + .excludePathPatterns("/admin/login", "/admin/api/login", "/admin/api/initialize"); registry.addInterceptor(memberAuthInterceptor()) .addPathPatterns("/member-tickets/**", "/members/**", "/auth/**", "/students/**") .excludePathPatterns("/auth/oauth2"); diff --git a/backend/src/main/java/com/festago/common/exception/ErrorCode.java b/backend/src/main/java/com/festago/common/exception/ErrorCode.java index d2e3b556f..9c57b4cab 100644 --- a/backend/src/main/java/com/festago/common/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/common/exception/ErrorCode.java @@ -7,16 +7,16 @@ public enum ErrorCode { NOT_ENTRY_TIME("입장 가능한 시간이 아닙니다."), EXPIRED_ENTRY_CODE("만료된 입장 코드입니다."), INVALID_ENTRY_CODE("올바르지 않은 입장코드입니다."), - INVALID_TICKET_OPEN_TIME("티켓은 공연 시작 전에 오픈되어야 합니다."), + INVALID_TICKET_OPEN_TIME("티켓 오픈 시간은 공연 시작 이전 이어야 합니다."), INVALID_STAGE_START_TIME("공연은 축제 기간 중에만 진행될 수 있습니다."), INVALID_MIN_TICKET_AMOUNT("티켓은 적어도 한장 이상 발급해야합니다."), LATE_TICKET_ENTRY_TIME("입장 시간은 공연 시간보다 빨라야합니다."), EARLY_TICKET_ENTRY_TIME("입장 시간은 공연 시작 12시간 이내여야 합니다."), EARLY_TICKET_ENTRY_THAN_OPEN("입장 시간은 티켓 오픈 시간 이후여야합니다."), TICKET_SOLD_OUT("매진된 티켓입니다."), - INVALID_FESTIVAL_START_DATE("축제 시작 일자는 과거일 수 없습니다."), - INVALID_FESTIVAL_DURATION("축제 시작 일자는 종료일자 이전이어야합니다."), - INVALID_TICKET_CREATE_TIME("티켓 예매 시작 후 새롭게 티켓을 발급할 수 없습니다."), + INVALID_FESTIVAL_DURATION("축제 시작 일은 종료일 이전이어야 합니다."), + INVALID_FESTIVAL_START_DATE("축제 시작 일은 과거일 수 없습니다."), + INVALID_TICKET_CREATE_TIME("티켓 오픈 시간 이후 새롭게 티켓을 발급할 수 없습니다."), OAUTH2_NOT_SUPPORTED_SOCIAL_TYPE("해당 OAuth2 제공자는 지원되지 않습니다."), RESERVE_TICKET_OVER_AMOUNT("예매 가능한 수량을 초과했습니다."), NEED_STUDENT_VERIFICATION("학생 인증이 필요합니다."), @@ -25,6 +25,10 @@ public enum ErrorCode { DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."), TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."), INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."), + DELETE_CONSTRAINT_FESTIVAL("공연이 등록된 축제는 삭제할 수 없습니다."), + DELETE_CONSTRAINT_STAGE("티켓이 등록된 공연은 삭제할 수 없습니다."), + DELETE_CONSTRAINT_SCHOOL("학생 또는 축제에 등록된 학교는 삭제할 수 없습니다."), + DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."), // 401 @@ -34,7 +38,6 @@ public enum ErrorCode { NEED_AUTH_TOKEN("로그인이 필요한 서비스입니다."), INCORRECT_PASSWORD_OR_ACCOUNT("비밀번호가 틀렸거나, 해당 계정이 없습니다."), DUPLICATE_ACCOUNT_USERNAME("해당 계정이 존재합니다."), - DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."), // 403 NOT_ENOUGH_PERMISSION("해당 권한이 없습니다."), diff --git a/backend/src/main/java/com/festago/common/util/Validator.java b/backend/src/main/java/com/festago/common/util/Validator.java new file mode 100644 index 000000000..481a29730 --- /dev/null +++ b/backend/src/main/java/com/festago/common/util/Validator.java @@ -0,0 +1,49 @@ +package com.festago.common.util; + +public final class Validator { + + private Validator() { + } + + /** + * 문자열의 최대 길이를 검증합니다. null 값은 무시됩니다. 최대 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 maxLength보다 작거나 같으면 예외를 던지지 않습니다. + * + * @param input 검증할 문자열 + * @param maxLength 검증할 문자열의 최대 길이 + * @param message 예외 메시지 + * @throws IllegalArgumentException 문자열의 길이가 초과되거나, 최대 길이가 0 이하이면 + */ + public static void maxLength(CharSequence input, int maxLength, String message) { + if (maxLength <= 0) { + throw new IllegalArgumentException("검증 길이는 0보다 커야합니다."); + } + // avoid NPE + if (input == null) { + return; + } + if (input.length() > maxLength) { + throw new IllegalArgumentException(message); + } + } + + /** + * 문자열의 최소 길이를 검증합니다. null 값은 무시됩니다. 최소 길이가 0 이하이면 예외를 던집니다. 문자열의 길이가 minLength보다 크거나 같으면 예외를 던지지 않습니다. + * + * @param input 검증할 문자열 + * @param minLength 검증할 문자열의 최소 길이 + * @param message 예외 메시지 + * @throws IllegalArgumentException 문자열의 길이가 작으면, 최대 길이가 0 이하이면 + */ + public static void minLength(CharSequence input, int minLength, String message) { + if (minLength <= 0) { + throw new IllegalArgumentException("검증 길이는 0보다 커야합니다."); + } + // avoid NPE + if (input == null) { + return; + } + if (input.length() < minLength) { + throw new IllegalArgumentException(message); + } + } +} diff --git a/backend/src/main/java/com/festago/festival/application/FestivalService.java b/backend/src/main/java/com/festago/festival/application/FestivalService.java index bcd5fe00d..4fe7c285f 100644 --- a/backend/src/main/java/com/festago/festival/application/FestivalService.java +++ b/backend/src/main/java/com/festago/festival/application/FestivalService.java @@ -9,14 +9,17 @@ import com.festago.festival.dto.FestivalCreateRequest; import com.festago.festival.dto.FestivalDetailResponse; import com.festago.festival.dto.FestivalResponse; +import com.festago.festival.dto.FestivalUpdateRequest; import com.festago.festival.dto.FestivalsResponse; import com.festago.festival.repository.FestivalRepository; import com.festago.school.domain.School; import com.festago.school.repository.SchoolRepository; import com.festago.stage.domain.Stage; import com.festago.stage.repository.StageRepository; +import java.time.Clock; import java.time.LocalDate; import java.util.List; +import org.springframework.dao.DataIntegrityViolationException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,18 +32,18 @@ public class FestivalService { private final FestivalRepository festivalRepository; private final StageRepository stageRepository; private final SchoolRepository schoolRepository; + private final Clock clock; public FestivalResponse create(FestivalCreateRequest request) { School school = schoolRepository.findById(request.schoolId()) .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND)); Festival festival = request.toEntity(school); validate(festival); - Festival newFestival = festivalRepository.save(festival); - return FestivalResponse.from(newFestival); + return FestivalResponse.from(festivalRepository.save(festival)); } private void validate(Festival festival) { - if (!festival.canCreate(LocalDate.now())) { + if (!festival.canCreate(LocalDate.now(clock))) { throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_START_DATE); } } @@ -53,11 +56,32 @@ public FestivalsResponse findAll() { @Transactional(readOnly = true) public FestivalDetailResponse findDetail(Long festivalId) { - Festival festival = festivalRepository.findById(festivalId) - .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND)); + Festival festival = findFestival(festivalId); List stages = stageRepository.findAllDetailByFestivalId(festivalId).stream() .sorted(comparing(Stage::getStartTime)) .toList(); return FestivalDetailResponse.of(festival, stages); } + + private Festival findFestival(Long festivalId) { + return festivalRepository.findById(festivalId) + .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND)); + } + + public void update(Long festivalId, FestivalUpdateRequest request) { + Festival festival = findFestival(festivalId); + festival.changeName(request.name()); + festival.changeThumbnail(request.thumbnail()); + festival.changeDate(request.startDate(), request.endDate()); + validate(festival); + } + + public void delete(Long festivalId) { + try { + festivalRepository.deleteById(festivalId); + festivalRepository.flush(); + } catch (DataIntegrityViolationException e) { + throw new BadRequestException(ErrorCode.DELETE_CONSTRAINT_FESTIVAL); + } + } } diff --git a/backend/src/main/java/com/festago/festival/domain/Festival.java b/backend/src/main/java/com/festago/festival/domain/Festival.java index 44b7bd684..9b0522c08 100644 --- a/backend/src/main/java/com/festago/festival/domain/Festival.java +++ b/backend/src/main/java/com/festago/festival/domain/Festival.java @@ -1,8 +1,7 @@ package com.festago.festival.domain; import com.festago.common.domain.BaseTimeEntity; -import com.festago.common.exception.BadRequestException; -import com.festago.common.exception.ErrorCode; +import com.festago.common.util.Validator; import com.festago.school.domain.School; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -14,6 +13,7 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.time.LocalDateTime; +import org.springframework.util.Assert; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -64,37 +64,26 @@ public Festival(Long id, String name, LocalDate startDate, LocalDate endDate, St } private void validate(String name, LocalDate startDate, LocalDate endDate, String thumbnail) { - checkNotNull(name, startDate, endDate, thumbnail); - checkLength(name, thumbnail); + validateName(name); + validateThumbnail(thumbnail); validateDate(startDate, endDate); } - private void checkNotNull(String name, LocalDate startDate, LocalDate endDate, String thumbnail) { - if (name == null || - startDate == null || - endDate == null || - thumbnail == null) { - throw new IllegalArgumentException("Festival 은 허용되지 않은 null 값으로 생성할 수 없습니다."); - } - } - - private void checkLength(String name, String thumbnail) { - if (overLength(name, 50) || - overLength(thumbnail, 255)) { - throw new IllegalArgumentException("Festival 의 필드로 허용된 길이를 넘은 column 을 넣을 수 없습니다."); - } + private void validateName(String name) { + Assert.notNull(name, "name은 null 값이 될 수 없습니다."); + Validator.maxLength(name, 50, "name은 50글자를 넘을 수 없습니다."); } - private boolean overLength(String target, int maxLength) { - if (target == null) { - return false; - } - return target.length() > maxLength; + private void validateThumbnail(String thumbnail) { + Assert.notNull(thumbnail, "thumbnail은 null 값이 될 수 없습니다."); + Validator.maxLength(thumbnail, 255, "thumbnail은 50글자를 넘을 수 없습니다."); } private void validateDate(LocalDate startDate, LocalDate endDate) { + Assert.notNull(startDate, "startDate는 null 값이 될 수 없습니다."); + Assert.notNull(endDate, "endDate는 null 값이 될 수 없습니다."); if (startDate.isAfter(endDate)) { - throw new BadRequestException(ErrorCode.INVALID_FESTIVAL_DURATION); + throw new IllegalArgumentException("축제 시작 일은 종료일 이전이어야 합니다."); } } @@ -107,6 +96,22 @@ public boolean isNotInDuration(LocalDateTime time) { return date.isBefore(startDate) || date.isAfter(endDate); } + public void changeName(String name) { + validateName(name); + this.name = name; + } + + public void changeThumbnail(String thumbnail) { + validateThumbnail(thumbnail); + this.thumbnail = thumbnail; + } + + public void changeDate(LocalDate startDate, LocalDate endDate) { + validateDate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java index 5ff6d92ea..8b6d0c950 100644 --- a/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java +++ b/backend/src/main/java/com/festago/festival/dto/FestivalDetailResponse.java @@ -7,6 +7,7 @@ public record FestivalDetailResponse( Long id, + Long schoolId, String name, LocalDate startDate, LocalDate endDate, @@ -19,6 +20,7 @@ public static FestivalDetailResponse of(Festival festival, List stages) { .toList(); return new FestivalDetailResponse( festival.getId(), + festival.getSchool().getId(), festival.getName(), festival.getStartDate(), festival.getEndDate(), diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java b/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java index d59f9ae93..ad4bac4ff 100644 --- a/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java +++ b/backend/src/main/java/com/festago/festival/dto/FestivalResponse.java @@ -5,6 +5,7 @@ public record FestivalResponse( Long id, + Long schoolId, String name, LocalDate startDate, LocalDate endDate, @@ -13,6 +14,7 @@ public record FestivalResponse( public static FestivalResponse from(Festival festival) { return new FestivalResponse( festival.getId(), + festival.getSchool().getId(), festival.getName(), festival.getStartDate(), festival.getEndDate(), diff --git a/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java b/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java new file mode 100644 index 000000000..2609b7451 --- /dev/null +++ b/backend/src/main/java/com/festago/festival/dto/FestivalUpdateRequest.java @@ -0,0 +1,16 @@ +package com.festago.festival.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; + +public record FestivalUpdateRequest( + @NotBlank(message = "name은 공백일 수 없습니다.") String name, + @NotNull(message = "startDate는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE) LocalDate startDate, + @NotNull(message = "endDate는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE) LocalDate endDate, + String thumbnail +) { + +} diff --git a/backend/src/main/java/com/festago/presentation/AdminController.java b/backend/src/main/java/com/festago/presentation/AdminController.java index c60987500..adb63cff4 100644 --- a/backend/src/main/java/com/festago/presentation/AdminController.java +++ b/backend/src/main/java/com/festago/presentation/AdminController.java @@ -11,21 +11,22 @@ import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.InternalServerException; -import com.festago.common.exception.UnauthorizedException; import com.festago.festival.application.FestivalService; import com.festago.festival.dto.FestivalCreateRequest; import com.festago.festival.dto.FestivalResponse; +import com.festago.festival.dto.FestivalUpdateRequest; import com.festago.school.application.SchoolService; import com.festago.school.dto.SchoolCreateRequest; import com.festago.school.dto.SchoolResponse; +import com.festago.school.dto.SchoolUpdateRequest; import com.festago.stage.application.StageService; import com.festago.stage.dto.StageCreateRequest; import com.festago.stage.dto.StageResponse; +import com.festago.stage.dto.StageUpdateRequest; import com.festago.ticket.application.TicketService; import com.festago.ticket.dto.TicketCreateRequest; import com.festago.ticket.dto.TicketCreateResponse; import io.swagger.v3.oas.annotations.Hidden; -import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import java.time.LocalDateTime; import java.time.ZoneId; @@ -35,19 +36,17 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; -import org.springframework.web.servlet.view.InternalResourceView; -import org.springframework.web.servlet.view.RedirectView; @RestController -@RequestMapping("/admin") +@RequestMapping("/admin/api") @Hidden @RequiredArgsConstructor public class AdminController { @@ -60,13 +59,6 @@ public class AdminController { private final SchoolService schoolService; private final Optional properties; - @PostMapping("/schools") - public ResponseEntity createSchool(@RequestBody @Valid SchoolCreateRequest request) { - SchoolResponse response = schoolService.create(request); - return ResponseEntity.ok() - .body(response); - } - @PostMapping("/festivals") public ResponseEntity createFestival(@RequestBody @Valid FestivalCreateRequest request) { FestivalResponse response = festivalService.create(request); @@ -74,6 +66,21 @@ public ResponseEntity createFestival(@RequestBody @Valid Festi .body(response); } + @PatchMapping("/festivals/{festivalId}") + public ResponseEntity updateFestival(@RequestBody @Valid FestivalUpdateRequest request, + @PathVariable Long festivalId) { + festivalService.update(festivalId, request); + return ResponseEntity.ok() + .build(); + } + + @DeleteMapping("/festivals/{festivalId}") + public ResponseEntity deleteFestival(@PathVariable Long festivalId) { + festivalService.delete(festivalId); + return ResponseEntity.ok() + .build(); + } + @PostMapping("/stages") public ResponseEntity createStage(@RequestBody @Valid StageCreateRequest request) { StageResponse response = stageService.create(request); @@ -81,6 +88,21 @@ public ResponseEntity createStage(@RequestBody @Valid StageCreate .body(response); } + @PatchMapping("/stages/{stageId}") + public ResponseEntity updateStage(@RequestBody @Valid StageUpdateRequest request, + @PathVariable Long stageId) { + stageService.update(stageId, request); + return ResponseEntity.ok() + .build(); + } + + @DeleteMapping("/stages/{stageId}") + public ResponseEntity deleteStage(@PathVariable Long stageId) { + stageService.delete(stageId); + return ResponseEntity.ok() + .build(); + } + @PostMapping("/tickets") public ResponseEntity createTicket(@RequestBody @Valid TicketCreateRequest request) { TicketCreateResponse response = ticketService.create(request); @@ -88,14 +110,26 @@ public ResponseEntity createTicket(@RequestBody @Valid Tic .body(response); } - @GetMapping - public ModelAndView adminPage() { - return new ModelAndView("admin/admin-page"); + @PostMapping("/schools") + public ResponseEntity createSchool(@RequestBody @Valid SchoolCreateRequest request) { + SchoolResponse response = schoolService.create(request); + return ResponseEntity.ok() + .body(response); } - @GetMapping("/login") - public ModelAndView loginPage() { - return new ModelAndView("admin/login"); + @PatchMapping("/schools/{schoolId}") + public ResponseEntity updateSchool(@RequestBody @Valid SchoolUpdateRequest request, + @PathVariable Long schoolId) { + schoolService.update(schoolId, request); + return ResponseEntity.ok() + .build(); + } + + @DeleteMapping("/schools/{schoolId}") + public ResponseEntity deleteSchool(@PathVariable Long schoolId) { + schoolService.delete(schoolId); + return ResponseEntity.ok() + .build(); } @PostMapping("/login") @@ -149,11 +183,6 @@ public ResponseEntity initializeRootAdmin(@RequestBody @Valid RootAdminIni .build(); } - @GetMapping("/signup") - public ModelAndView signupPage() { - return new ModelAndView("admin/signup"); - } - @PostMapping("/signup") public ResponseEntity signupAdminAccount(@RequestBody @Valid AdminSignupRequest request, @Admin Long adminId) { @@ -161,13 +190,4 @@ public ResponseEntity signupAdminAccount(@RequestBody @Vali return ResponseEntity.ok() .body(response); } - - @ExceptionHandler(UnauthorizedException.class) - public View handle(UnauthorizedException e, HttpServletResponse response) { - if (e.getErrorCode() == ErrorCode.EXPIRED_AUTH_TOKEN) { - return new RedirectView("/admin/login"); - } - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - return new InternalResourceView("/error/404"); - } } diff --git a/backend/src/main/java/com/festago/presentation/AdminViewController.java b/backend/src/main/java/com/festago/presentation/AdminViewController.java new file mode 100644 index 000000000..994e2e614 --- /dev/null +++ b/backend/src/main/java/com/festago/presentation/AdminViewController.java @@ -0,0 +1,70 @@ +package com.festago.presentation; + +import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.UnauthorizedException; +import io.swagger.v3.oas.annotations.Hidden; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.InternalResourceView; +import org.springframework.web.servlet.view.RedirectView; + +@Controller +@RequestMapping("/admin") +@Hidden +public class AdminViewController { + + @GetMapping + public String adminPage() { + return "admin/admin-page"; + } + + @GetMapping("/login") + public String loginPage() { + return "admin/login"; + } + + @GetMapping("/signup") + public String signupPage() { + return "admin/signup"; + } + + @GetMapping("/festivals") + public String manageFestivalPage() { + return "admin/festival/manage-festival"; + } + + @GetMapping("/festivals/{festivalId}") + public String manageFestivalDetailPage(@PathVariable String festivalId) { + return "admin/festival/manage-festival-detail"; + } + + @GetMapping("/schools") + public String manageSchoolPage() { + return "admin/school/manage-school"; + } + + @GetMapping("/schools/{schoolId}") + public String manageSchoolDetailPage(@PathVariable String schoolId) { + return "admin/school/manage-school-detail"; + } + + @GetMapping("/stages/{stageId}") + public String manageStagePage(@PathVariable String stageId) { + return "admin/stage/manage-stage-detail"; + } + + @ExceptionHandler(UnauthorizedException.class) + public View handle(UnauthorizedException e, HttpServletResponse response) { + + if (e.getErrorCode() == ErrorCode.EXPIRED_AUTH_TOKEN) { + return new RedirectView("/admin/login"); + } + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + return new InternalResourceView("/error/404"); + } +} diff --git a/backend/src/main/java/com/festago/presentation/SchoolController.java b/backend/src/main/java/com/festago/presentation/SchoolController.java index 884b50ff7..8667e4597 100644 --- a/backend/src/main/java/com/festago/presentation/SchoolController.java +++ b/backend/src/main/java/com/festago/presentation/SchoolController.java @@ -1,10 +1,12 @@ package com.festago.presentation; import com.festago.school.application.SchoolService; +import com.festago.school.dto.SchoolResponse; import com.festago.school.dto.SchoolsResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,6 +19,13 @@ public class SchoolController { @GetMapping public ResponseEntity findAll() { - return ResponseEntity.ok(schoolService.findAll()); + return ResponseEntity.ok() + .body(schoolService.findAll()); + } + + @GetMapping("/{schoolId}") + public ResponseEntity findById(@PathVariable Long schoolId) { + return ResponseEntity.ok() + .body(schoolService.findById(schoolId)); } } diff --git a/backend/src/main/java/com/festago/presentation/StageController.java b/backend/src/main/java/com/festago/presentation/StageController.java index 53c85716a..d94a407b9 100644 --- a/backend/src/main/java/com/festago/presentation/StageController.java +++ b/backend/src/main/java/com/festago/presentation/StageController.java @@ -1,5 +1,7 @@ package com.festago.presentation; +import com.festago.stage.application.StageService; +import com.festago.stage.dto.StageResponse; import com.festago.ticket.application.TicketService; import com.festago.ticket.dto.StageTicketsResponse; import io.swagger.v3.oas.annotations.Operation; @@ -18,6 +20,7 @@ public class StageController { private final TicketService ticketService; + private final StageService stageService; @GetMapping("/{stageId}/tickets") @Operation(description = "특정 무대의 티켓 정보를 보여준다.", summary = "무대 티켓 목록 조회") @@ -26,4 +29,12 @@ public ResponseEntity findStageTickets(@PathVariable Long return ResponseEntity.ok() .body(response); } + + @GetMapping("/{stageId}") + @Operation(description = "특정 무대의 정보를 보여준다.", summary = "무대 정보 조회") + public ResponseEntity findStageDetail(@PathVariable Long stageId) { + StageResponse response = stageService.findDetail(stageId); + return ResponseEntity.ok() + .body(response); + } } diff --git a/backend/src/main/java/com/festago/school/application/SchoolService.java b/backend/src/main/java/com/festago/school/application/SchoolService.java index 0477121a7..f8fbe2792 100644 --- a/backend/src/main/java/com/festago/school/application/SchoolService.java +++ b/backend/src/main/java/com/festago/school/application/SchoolService.java @@ -2,12 +2,15 @@ import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; +import com.festago.common.exception.NotFoundException; import com.festago.school.domain.School; import com.festago.school.dto.SchoolCreateRequest; import com.festago.school.dto.SchoolResponse; +import com.festago.school.dto.SchoolUpdateRequest; import com.festago.school.dto.SchoolsResponse; import com.festago.school.repository.SchoolRepository; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,9 +21,26 @@ public class SchoolService { private final SchoolRepository schoolRepository; + @Transactional(readOnly = true) + public SchoolsResponse findAll() { + return SchoolsResponse.from(schoolRepository.findAll()); + } + + @Transactional(readOnly = true) + public SchoolResponse findById(Long id) { + return SchoolResponse.from(findSchool(id)); + } + + private School findSchool(Long id) { + return schoolRepository.findById(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND)); + } + public SchoolResponse create(SchoolCreateRequest request) { validateSchool(request); - School school = schoolRepository.save(new School(request.domain(), request.name())); + String domain = request.domain(); + String name = request.name(); + School school = schoolRepository.save(new School(domain, name)); return SchoolResponse.from(school); } @@ -30,8 +50,19 @@ private void validateSchool(SchoolCreateRequest request) { } } - @Transactional(readOnly = true) - public SchoolsResponse findAll() { - return SchoolsResponse.from(schoolRepository.findAll()); + public void update(Long schoolId, SchoolUpdateRequest request) { + School school = findSchool(schoolId); + school.changeName(request.name()); + school.changeDomain(request.domain()); + } + + public void delete(Long schoolId) { + // TODO 지금은 외래키 제약조건 때문에 참조하는 다른 엔티티가 있으면 예외가 발생하지만, 추후 이미 가입된 학생이 있다는 등 예외가 필요할듯 + try { + schoolRepository.deleteById(schoolId); + schoolRepository.flush(); + } catch (DataIntegrityViolationException e) { + throw new BadRequestException(ErrorCode.DELETE_CONSTRAINT_SCHOOL); + } } } diff --git a/backend/src/main/java/com/festago/school/domain/School.java b/backend/src/main/java/com/festago/school/domain/School.java index 84534ddc5..a261c838e 100644 --- a/backend/src/main/java/com/festago/school/domain/School.java +++ b/backend/src/main/java/com/festago/school/domain/School.java @@ -1,8 +1,7 @@ package com.festago.school.domain; import com.festago.common.domain.BaseTimeEntity; -import com.festago.common.exception.ErrorCode; -import com.festago.common.exception.InternalServerException; +import com.festago.common.util.Validator; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -12,6 +11,7 @@ import jakarta.validation.constraints.Size; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.springframework.util.Assert; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -43,29 +43,28 @@ public School(Long id, String domain, String name) { } private void validate(String domain, String name) { - checkNotNull(domain, name); - checkLength(domain, name); + validateDomain(domain); + validateName(name); } - private void checkNotNull(String domain, String name) { - if (domain == null || - name == null) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); - } + private void validateDomain(String domain) { + Assert.notNull(domain, "domain은 null 값이 될 수 없습니다."); + Validator.maxLength(domain, 50, "domain은 50글자를 넘을 수 없습니다."); } - private void checkLength(String domain, String name) { - if (overLength(domain, 50) || - overLength(name, 255)) { - throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); - } + private void validateName(String name) { + Assert.notNull(name, "name은 null 값이 될 수 없습니다."); + Validator.maxLength(name, 255, "name은 255글자를 넘을 수 없습니다."); } - private boolean overLength(String target, int maxLength) { - if (target == null) { - return false; - } - return target.length() > maxLength; + public void changeDomain(String domain) { + validateDomain(domain); + this.domain = domain; + } + + public void changeName(String name) { + validateName(name); + this.name = name; } public Long getId() { diff --git a/backend/src/main/java/com/festago/school/dto/SchoolUpdateRequest.java b/backend/src/main/java/com/festago/school/dto/SchoolUpdateRequest.java new file mode 100644 index 000000000..a94bc101b --- /dev/null +++ b/backend/src/main/java/com/festago/school/dto/SchoolUpdateRequest.java @@ -0,0 +1,10 @@ +package com.festago.school.dto; + +import jakarta.validation.constraints.NotBlank; + +public record SchoolUpdateRequest( + @NotBlank(message = "domain은 공백일 수 없습니다.") String domain, + @NotBlank(message = "name은 공백일 수 없습니다.") String name +) { + +} diff --git a/backend/src/main/java/com/festago/school/repository/SchoolRepository.java b/backend/src/main/java/com/festago/school/repository/SchoolRepository.java index c264fa517..43f1d8823 100644 --- a/backend/src/main/java/com/festago/school/repository/SchoolRepository.java +++ b/backend/src/main/java/com/festago/school/repository/SchoolRepository.java @@ -6,5 +6,4 @@ public interface SchoolRepository extends JpaRepository { boolean existsByDomainOrName(String domain, String name); - } diff --git a/backend/src/main/java/com/festago/stage/application/StageService.java b/backend/src/main/java/com/festago/stage/application/StageService.java index 391bc2d78..75dfc63ab 100644 --- a/backend/src/main/java/com/festago/stage/application/StageService.java +++ b/backend/src/main/java/com/festago/stage/application/StageService.java @@ -1,5 +1,6 @@ package com.festago.stage.application; +import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; import com.festago.common.exception.NotFoundException; import com.festago.festival.domain.Festival; @@ -7,8 +8,10 @@ import com.festago.stage.domain.Stage; import com.festago.stage.dto.StageCreateRequest; import com.festago.stage.dto.StageResponse; +import com.festago.stage.dto.StageUpdateRequest; import com.festago.stage.repository.StageRepository; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,7 +24,7 @@ public class StageService { private final FestivalRepository festivalRepository; public StageResponse create(StageCreateRequest request) { - Festival festival = findFestivalById(request.festivalId()); + Festival festival = findFestival(request.festivalId()); Stage newStage = stageRepository.save(new Stage( request.startTime(), request.lineUp(), @@ -31,8 +34,33 @@ public StageResponse create(StageCreateRequest request) { return StageResponse.from(newStage); } - private Festival findFestivalById(Long festivalId) { + private Festival findFestival(Long festivalId) { return festivalRepository.findById(festivalId) .orElseThrow(() -> new NotFoundException(ErrorCode.FESTIVAL_NOT_FOUND)); } + + public StageResponse findDetail(Long stageId) { + Stage stage = findStage(stageId); + return StageResponse.from(stage); + } + + private Stage findStage(Long stageId) { + return stageRepository.findById(stageId) + .orElseThrow(() -> new NotFoundException(ErrorCode.STAGE_NOT_FOUND)); + } + + public void update(Long stageId, StageUpdateRequest request) { + Stage stage = findStage(stageId); + stage.changeTime(request.startTime(), request.ticketOpenTime()); + stage.changeLineUp(request.lineUp()); + } + + public void delete(Long stageId) { + try { + stageRepository.deleteById(stageId); + stageRepository.flush(); + } catch (DataIntegrityViolationException e) { + throw new BadRequestException(ErrorCode.DELETE_CONSTRAINT_STAGE); + } + } } diff --git a/backend/src/main/java/com/festago/stage/domain/Stage.java b/backend/src/main/java/com/festago/stage/domain/Stage.java index d15327e12..5c5cea7d7 100644 --- a/backend/src/main/java/com/festago/stage/domain/Stage.java +++ b/backend/src/main/java/com/festago/stage/domain/Stage.java @@ -3,6 +3,7 @@ import com.festago.common.domain.BaseTimeEntity; import com.festago.common.exception.BadRequestException; import com.festago.common.exception.ErrorCode; +import com.festago.common.util.Validator; import com.festago.festival.domain.Festival; import com.festago.ticket.domain.Ticket; import jakarta.persistence.Entity; @@ -19,6 +20,7 @@ import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.springframework.util.Assert; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -63,45 +65,45 @@ public Stage(Long id, LocalDateTime startTime, String lineUp, LocalDateTime tick } private void validate(LocalDateTime startTime, String lineUp, LocalDateTime ticketOpenTime, Festival festival) { - checkNotNull(startTime, ticketOpenTime, festival); - checkLength(lineUp); + validateLineUp(lineUp); + validateFestival(festival); validateTime(startTime, ticketOpenTime, festival); } - private void checkNotNull(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { - if (startTime == null || - ticketOpenTime == null || - festival == null) { - throw new IllegalArgumentException("Stage 는 허용되지 않은 null 값으로 생성할 수 없습니다."); - } - } - - private void checkLength(String lineUp) { - if (overLength(lineUp, 255)) { - throw new IllegalArgumentException("Stage 의 필드로 허용된 범위를 넘은 column 을 넣을 수 없습니다."); - } + private void validateLineUp(String lineUp) { + Validator.maxLength(lineUp, 255, "lineUp은 50글자를 넘을 수 없습니다."); } - private boolean overLength(String target, int maxLength) { - if (target == null) { - return false; - } - return target.length() > maxLength; + private void validateFestival(Festival festival) { + Assert.notNull(festival, "festival은 null 값이 될 수 없습니다."); } private void validateTime(LocalDateTime startTime, LocalDateTime ticketOpenTime, Festival festival) { + Assert.notNull(startTime, "startTime은 null 값이 될 수 없습니다."); + Assert.notNull(ticketOpenTime, "ticketOpenTime은 null 값이 될 수 없습니다."); + if (ticketOpenTime.isAfter(startTime) || ticketOpenTime.isEqual(startTime)) { + throw new IllegalArgumentException("티켓 오픈 시간은 공연 시작 이전 이어야 합니다."); + } if (festival.isNotInDuration(startTime)) { throw new BadRequestException(ErrorCode.INVALID_STAGE_START_TIME); } - if (ticketOpenTime.isAfter(startTime) || ticketOpenTime.isEqual(startTime)) { - throw new BadRequestException(ErrorCode.INVALID_TICKET_OPEN_TIME); - } } public boolean isStart(LocalDateTime currentTime) { return currentTime.isAfter(startTime); } + public void changeTime(LocalDateTime startTime, LocalDateTime ticketOpenTime) { + validateTime(startTime, ticketOpenTime, this.festival); + this.startTime = startTime; + this.ticketOpenTime = ticketOpenTime; + } + + public void changeLineUp(String lineUp) { + validateLineUp(lineUp); + this.lineUp = lineUp; + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/festago/stage/dto/StageResponse.java b/backend/src/main/java/com/festago/stage/dto/StageResponse.java index 1aea96eaa..4355a0a11 100644 --- a/backend/src/main/java/com/festago/stage/dto/StageResponse.java +++ b/backend/src/main/java/com/festago/stage/dto/StageResponse.java @@ -5,9 +5,18 @@ public record StageResponse( Long id, - LocalDateTime startTime) { + Long festivalId, + LocalDateTime startTime, + LocalDateTime ticketOpenTime, + String lineUp) { public static StageResponse from(Stage stage) { - return new StageResponse(stage.getId(), stage.getStartTime()); + return new StageResponse( + stage.getId(), + stage.getFestival().getId(), + stage.getStartTime(), + stage.getTicketOpenTime(), + stage.getLineUp() + ); } } diff --git a/backend/src/main/java/com/festago/stage/dto/StageUpdateRequest.java b/backend/src/main/java/com/festago/stage/dto/StageUpdateRequest.java new file mode 100644 index 000000000..3a5241eb0 --- /dev/null +++ b/backend/src/main/java/com/festago/stage/dto/StageUpdateRequest.java @@ -0,0 +1,14 @@ +package com.festago.stage.dto; + +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; + +public record StageUpdateRequest( + @NotNull(message = "startTime는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime startTime, + @NotNull(message = "ticketOpenTime는 null일 수 없습니다.") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime ticketOpenTime, + String lineUp +) { + +} diff --git a/backend/src/main/resources/static/js/admin/admin-page.js b/backend/src/main/resources/static/js/admin/admin-page.js index 9f740fb52..4de514e4e 100644 --- a/backend/src/main/resources/static/js/admin/admin-page.js +++ b/backend/src/main/resources/static/js/admin/admin-page.js @@ -1,279 +1,66 @@ -// Function to fetch data and update dataSection -function fetchDataAndUpdateDataSection() { - fetch("/admin/data") - .then(response => { - if (!response.ok) { - throw new Error("응답을 가져올 수 없습니다."); - } - return response.json(); - }) - .then(data => { - const dataSection = document.getElementById("dataSection"); - - // Clear existing data before appending new data - dataSection.innerHTML = ""; - - // 1. 학교 생성 요청 데이터 섹션 - const schoolDataDiv = createDataSection( - "학교 목록", - data.adminSchools - ); - dataSection.appendChild(schoolDataDiv); - - // 2. 축제 생성 요청 데이터 섹션 - const festivalDataDiv = createDataSection( - "축제 목록", - data.adminFestivalResponse - ); - dataSection.appendChild(festivalDataDiv); - - // 3. 공연 생성 요청 데이터 섹션 - const stageDataDiv = createDataSection( - "공연 목록", - data.adminStageResponse - ); - dataSection.appendChild(stageDataDiv); +function init() { + const serverVersionBtn = document.getElementById("serverVersionBtn"); + const infoLogBtn = document.getElementById("infoLogBtn"); + const warnLogBtn = document.getElementById("warnLogBtn"); + const errorLogBtn = document.getElementById("errorLogBtn"); + + serverVersionBtn.addEventListener("click", showServerVersion); + infoLogBtn.addEventListener("click", executeInfoLog); + warnLogBtn.addEventListener("click", executeWarnLog); + errorLogBtn.addEventListener("click", executeErrorLog); +} - // 4. 티켓 생성 요청 데이터 섹션 - const ticketDataDiv = createDataSection( - "티켓 목록", - data.adminTickets - ); - dataSection.appendChild(ticketDataDiv); - }) - .catch(error => { +function showServerVersion() { + fetch("/admin/api/version") + .then(res => { + if (res.ok) { + return res.text(); + } + throw new Error("서버에 연결할 수 없습니다."); + }).then(body => { + alert(body); + }).catch(error => { alert(error.message); - }); + }) } -// Call the fetchDataAndUpdateDataSection function on page load -fetchDataAndUpdateDataSection(); - -function createDataSection(sectionTitle, data) { - const dataDiv = document.createElement("div"); - dataDiv.classList.add("dataDiv"); // Add the "dataDiv" class to the data div - - const title = document.createElement("h3"); - title.textContent = sectionTitle; - dataDiv.appendChild(title); - - const table = createTable(data); - dataDiv.appendChild(table); - - return dataDiv; +function executeInfoLog() { + fetch("/admin/api/info") + .then(res => { + if (res.status !== 400) { + throw new Error("서버에 연결할 수 없습니다."); + } + }).then(() => { + alert("실행 완료"); + }).catch(error => { + alert(error.message); + }) } -function createTable(data) { - const table = document.createElement("table"); - const tableHead = document.createElement("thead"); - const tableBody = document.createElement("tbody"); - - // 테이블 헤더 생성 - const headerRow = document.createElement("tr"); - for (const key in data[0]) { - const th = document.createElement("th"); - th.textContent = key; - headerRow.appendChild(th); - } - tableHead.appendChild(headerRow); - - // 테이블 데이터 생성 - data.forEach(item => { - const row = document.createElement("tr"); - for (const key in item) { - const cell = document.createElement("td"); - - if (typeof item[key] === "object") { - // If the value is an object (entryTimeAmount), format it with new lines - cell.textContent = formatEntryTimeAmount(item[key]); - cell.style.whiteSpace = "pre-line"; // Apply white-space: pre-line; style to allow line breaks - } else { - // If the value is not an object, display it normally - cell.textContent = item[key]; - } - - row.appendChild(cell); +function executeWarnLog() { + fetch("/admin/api/warn") + .then(res => { + if (res.status !== 500) { + throw new Error("서버에 연결할 수 없습니다."); } - tableBody.appendChild(row); - }); - - table.appendChild(tableHead); - table.appendChild(tableBody); - return table; + }).then(() => { + alert("실행 완료"); + }).catch(error => { + alert(error.message); + }) } -// Helper function to format entryTimeAmount object and sort by time -function formatEntryTimeAmount(entryTimeAmount) { - // Convert the object to an array of [time, amount] pairs - const entryTimeAmountArray = Object.entries(entryTimeAmount); - - // Sort the array based on time - entryTimeAmountArray.sort((a, b) => a[0].localeCompare(b[0])); - - // Format the sorted array as a string with line breaks - let formattedString = ""; - entryTimeAmountArray.forEach(([time, amount]) => { - formattedString += `${time}: ${amount}\n`; - }); - - return formattedString; +function executeErrorLog() { + fetch("/admin/api/error") + .then(res => { + if (res.status !== 500) { + throw new Error("서버에 연결할 수 없습니다."); + } + }).then(() => { + alert("실행 완료"); + }).catch(error => { + alert(error.message); + }) } -// 학교 생성 버튼 클릭 시 요청 보내기 -document.getElementById("createSchoolForm").addEventListener("submit", - function (event) { - event.preventDefault(); - const formData = new FormData(event.target); - const festivalData = { - name: formData.get("schoolName"), - domain: formData.get("domain") - }; - - fetch("/admin/schools", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(festivalData) - }) - .then(response => { - if (response.ok) { - return response.json(); - } else { - return response.json().then(data => { - throw new Error(data.message || "학교 생성에 실패하였습니다."); - }); - } - }) - .then(data => { - // 성공: 알림창 표시하고 폼 필드 초기화 - alert("학교가 성공적으로 생성되었습니다!"); - // Fetch data again and update dataSection - fetchDataAndUpdateDataSection(); - }) - .catch(error => { - // 오류: 알림창 표시 - alert(error.message); - }); - }); - -// 축제 생성 버튼 클릭 시 요청 보내기 -document.getElementById("createFestivalForm").addEventListener("submit", - function (event) { - event.preventDefault(); - const formData = new FormData(event.target); - const festivalData = { - name: formData.get("name"), - startDate: formData.get("startDate"), - endDate: formData.get("endDate"), - thumbnail: formData.get("thumbnail"), - schoolId: formData.get("schoolId") - }; - - fetch("/admin/festivals", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(festivalData) - }) - .then(response => { - if (response.ok) { - return response.json(); - } else { - return response.json().then(data => { - throw new Error(data.message || "축제 생성에 실패하였습니다."); - }); - } - }) - .then(data => { - // 성공: 알림창 표시하고 폼 필드 초기화 - alert("축제가 성공적으로 생성되었습니다!"); - // Fetch data again and update dataSection - fetchDataAndUpdateDataSection(); - }) - .catch(error => { - // 오류: 알림창 표시 - alert(error.message); - }); - }); - -// 공연 생성 버튼 클릭 시 요청 보내기 -document.getElementById("createPerformanceForm").addEventListener("submit", - function (event) { - event.preventDefault(); - const formData = new FormData(event.target); - const performanceData = { - startTime: formData.get("startTime"), - lineUp: formData.get("lineUp"), - ticketOpenTime: formData.get("ticketOpenTime"), - festivalId: formData.get("festivalId") - }; - - fetch("/admin/stages", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(performanceData) - }) - .then(response => { - if (response.ok) { - return response.json(); - } else { - return response.json().then(data => { - throw new Error(data.message || "공연 생성에 실패하였습니다."); - }); - } - }) - .then(data => { - // 성공: 알림창 표시하고 폼 필드 초기화 - alert("공연이 성공적으로 생성되었습니다!"); - // Fetch data again and update dataSection - fetchDataAndUpdateDataSection(); - }) - .catch(error => { - // 오류: 알림창 표시 - alert(error.message); - }); - }); - -// 티켓 생성 버튼 클릭 시 요청 보내기 -document.getElementById("createTicketForm").addEventListener("submit", - function (event) { - event.preventDefault(); - const formData = new FormData(event.target); - const ticketData = { - stageId: formData.get("stageId"), - ticketType: formData.get("ticketType"), - amount: formData.get("amount"), - entryTime: formData.get("entryTime") - }; - - fetch("/admin/tickets", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(ticketData) - }) - .then(response => { - if (response.ok) { - return response.json(); - } else { - return response.json().then(data => { - throw new Error(data.message || "티켓 생성에 실패하였습니다."); - }); - } - }) - .then(data => { - // 성공: 알림창 표시하고 폼 필드 초기화 - alert("티켓이 성공적으로 생성되었습니다!"); - // Fetch data again and update dataSection - fetchDataAndUpdateDataSection(); - }) - .catch(error => { - // 오류: 알림창 표시 - alert(error.message); - }); - }); +init(); diff --git a/backend/src/main/resources/static/js/admin/festival/common-festival.js b/backend/src/main/resources/static/js/admin/festival/common-festival.js new file mode 100644 index 000000000..dd55774cc --- /dev/null +++ b/backend/src/main/resources/static/js/admin/festival/common-festival.js @@ -0,0 +1,14 @@ +export function validateFestival(festivalData) { + const startDate = new Date(festivalData.startDate); + const endDate = new Date(festivalData.endDate); + let hasError = false; + if (startDate > endDate) { + document.getElementById("festivalEndDate").classList.add("is-invalid"); + document.getElementById("festivalEndDate-feedback") + .textContent = "종료일은 시작일보다 이후 이어야 합니다." + hasError = true; + } + if (hasError) { + throw new Error("검증이 실패하였습니다."); + } +} diff --git a/backend/src/main/resources/static/js/admin/festival/manage-festival-detail.js b/backend/src/main/resources/static/js/admin/festival/manage-festival-detail.js new file mode 100644 index 000000000..39dd65855 --- /dev/null +++ b/backend/src/main/resources/static/js/admin/festival/manage-festival-detail.js @@ -0,0 +1,234 @@ +import {validateFestival} from "./common-festival.js" +import {getResourceId} from "../../common/UrlParser.js"; + +const deleteConfirmModal = new bootstrap.Modal( + document.getElementById("deleteConfirmModal")); + +function fetchFestival() { + const idInput = document.getElementById("id"); + const fakeIdInput = document.getElementById("fakeId"); + const schoolIdInput = document.getElementById("schoolId"); + const fakeSchoolIdInput = document.getElementById("fakeSchoolId"); + const nameInput = document.getElementById("name"); + const thumbnailInput = document.getElementById("thumbnail"); + const startDateInput = document.getElementById("festivalStartDate"); + const endDateInput = document.getElementById("festivalEndDate"); + const updateBtn = document.getElementById("festivalUpdateBtn"); + const deleteBtn = document.getElementById("festivalDeleteBtn"); + const festivalId = getResourceId(new URL(window.location.href)); + const errorModal = new bootstrap.Modal(document.getElementById("errorModal")); + + fetch(`/festivals/${festivalId}`).then(res => { + if (!res.ok) { + nameInput.setAttribute("disabled", ""); + thumbnailInput.setAttribute("disabled", ""); + startDateInput.setAttribute("disabled", ""); + endDateInput.setAttribute("disabled", ""); + updateBtn.setAttribute("disabled", ""); + deleteBtn.setAttribute("disabled", ""); + return res.json().then(data => { + throw new Error(data.message || data.detail) + }) + } + return res.json(); + }).then(festival => { + idInput.value = festival.id; + fakeIdInput.value = festival.id; + schoolIdInput.value = festival.schoolId + fakeSchoolIdInput.value = festival.schoolId + nameInput.value = festival.name; + thumbnailInput.value = festival.thumbnail; + startDateInput.value = festival.startDate; + endDateInput.value = festival.endDate; + renderStages(festival.stages) + }).catch(error => { + const errorModalBody = document.getElementById("errorModalBody"); + errorModalBody.textContent = error.message; + errorModal.show(); + }) +} + +fetchFestival(); + +function init() { + const festivalUpdateForm = document.getElementById("festivalUpdateForm"); + const deleteBtn = document.getElementById("festivalDeleteBtn"); + const actualDeleteBtn = document.getElementById("actualDeleteBtn"); + const stageCreateFrom = document.getElementById("stageCreateForm"); + + festivalUpdateForm.addEventListener("submit", updateFestival); + deleteBtn.addEventListener("click", openDeleteConfirmModal); + actualDeleteBtn.addEventListener("click", deleteFestival); + stageCreateFrom.addEventListener("submit", createStage); +} + +function updateFestival(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const festivalData = { + name: formData.get("name"), + startDate: formData.get("festivalStartDate"), + endDate: formData.get("festivalEndDate"), + thumbnail: formData.get("thumbnail"), + }; + validateFestival(festivalData) + const festivalId = formData.get("id"); + fetch(`/admin/api/festivals/${festivalId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(festivalData) + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "축제 수정에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("축제가 성공적으로 수정되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +init(); + +function openDeleteConfirmModal() { + deleteConfirmModal.show(); +} + +function deleteFestival() { + const idInput = document.getElementById("id"); + const festivalId = idInput.value; + fetch(`/admin/api/festivals/${festivalId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + } + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "축제 삭제에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("축제가 성공적으로 삭제되었습니다!"); + location.replace("/admin/festivals"); + }) + .catch(error => { + deleteConfirmModal.hide(); + alert(error.message); + }); +} + +function renderStages(stages) { + const stageGrid = document.getElementById("stageGrid"); + for (const stage of stages) { + const row = document.createElement("div"); + row.classList.add("row", "align-items-center", "gx-0", "py-1", + "border-top"); + + const idColumn = document.createElement("div"); + idColumn.classList.add("col-1"); + idColumn.textContent = stage.id; + + const startTimeColumn = document.createElement("div"); + startTimeColumn.classList.add("col-3"); + startTimeColumn.textContent = stage.startTime; + + const ticketOpenTimeColumn = document.createElement("div"); + ticketOpenTimeColumn.classList.add("col-3"); + ticketOpenTimeColumn.textContent = stage.ticketOpenTime; + + const lineUpColumn = document.createElement("div"); + lineUpColumn.classList.add("col-3"); + lineUpColumn.textContent = stage.lineUp; + + const buttonColumn = document.createElement("div"); + buttonColumn.classList.add("col-2") + const button = document.createElement("a"); + button.classList.add("btn", "btn-primary"); + button.setAttribute("href", `/admin/stages/${stage.id}`); + button.textContent = "편집"; + buttonColumn.append(button); + + row.append(idColumn, startTimeColumn, ticketOpenTimeColumn, lineUpColumn, + buttonColumn); + stageGrid.append(row); + } +} + +function createStage(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const stageData = { + festivalId: document.getElementById("id").value, + startTime: formData.get("stageStartTime"), + ticketOpenTime: formData.get("ticketOpenTime"), + lineUp: formData.get("lineUp"), + }; + validateStage(stageData); + + fetch("/admin/api/stages", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(stageData) + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.json().then(data => { + throw new Error(data.message || "공연 생성에 실패하였습니다."); + }); + } + }) + .then(data => { + alert("공연이 성공적으로 생성되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +function validateStage(stageData) { + const stageStartTime = new Date(stageData.startTime); + const ticketOpenTime = new Date(stageData.ticketOpenTime); + const now = new Date(); + let hasError = false; + if (stageStartTime <= ticketOpenTime) { + document.getElementById("ticketOpenTime").classList.add("is-invalid"); + document.getElementById("ticketOpenTime-feedback") + .textContent = "티켓 오픈 시간은 공연 시작 이전 이어야 합니다." + hasError = true; + } + if (stageStartTime < now) { + document.getElementById("stageStartTime").classList.add("is-invalid"); + document.getElementById("stageStartTime-feedback") + .textContent = "공연 시작 시간은 현재보다 이후 이어야 합니다." + hasError = true; + } + if (ticketOpenTime < now) { + document.getElementById("ticketOpenTime").classList.add("is-invalid"); + document.getElementById("ticketOpenTime-feedback") + .textContent = "티켓 오픈 시간은 현재보다 이후 이어야 합니다." + hasError = true; + } + if (hasError) { + throw new Error("검증이 실패하였습니다."); + } +} diff --git a/backend/src/main/resources/static/js/admin/festival/manage-festival.js b/backend/src/main/resources/static/js/admin/festival/manage-festival.js new file mode 100644 index 000000000..03e271afb --- /dev/null +++ b/backend/src/main/resources/static/js/admin/festival/manage-festival.js @@ -0,0 +1,123 @@ +import {validateFestival} from "./common-festival.js" + +function fetchFestivals() { + const festivalGrid = document.getElementById("festivalGrid"); + + fetch("/festivals").then(res => { + if (!res.ok) { + throw new Error("서버에 연결할 수 없습니다.") + } + return res.json(); + }).then(data => { + const festivals = data.festivals; + for (const festival of festivals) { + const row = document.createElement("div"); + row.classList.add("row", "align-items-center", "gx-0", "py-1", + "border-top"); + + const idColumn = document.createElement("div"); + idColumn.classList.add("col-1"); + idColumn.textContent = festival.id; + + const schoolIdColumn = document.createElement("div"); + schoolIdColumn.classList.add("col-1"); + schoolIdColumn.textContent = festival.schoolId; + + const nameColumn = document.createElement("div"); + nameColumn.classList.add("col-2"); + nameColumn.textContent = festival.name; + + const thumbnailColumn = document.createElement("div"); + thumbnailColumn.classList.add("col-2"); + thumbnailColumn.textContent = festival.thumbnail; + + const startDateColumn = document.createElement("div"); + startDateColumn.classList.add("col-2"); + startDateColumn.textContent = festival.startDate; + + const endDateColumn = document.createElement("div"); + endDateColumn.classList.add("col-2"); + endDateColumn.textContent = festival.endDate; + + const buttonColumn = document.createElement("div"); + buttonColumn.classList.add("col-2") + const button = document.createElement("a"); + button.classList.add("btn", "btn-primary"); + button.setAttribute("href", `festivals/${festival.id}`); + button.textContent = "편집"; + buttonColumn.append(button); + + row.append(idColumn, schoolIdColumn, nameColumn, thumbnailColumn, + startDateColumn, endDateColumn, buttonColumn); + festivalGrid.append(row); + } + }) +} + +fetchFestivals(); + +function fetchSchools() { + const schoolSelect = document.getElementById("schoolSelect"); + + fetch("/schools").then(res => { + if (!res.ok) { + throw new Error("서버에 연결할 수 없습니다.") + } + return res.json(); + }).then(data => { + const schools = data.schools; + for (const school of schools) { + const option = document.createElement("option"); + const schoolId = school.id; + option.textContent = `${school.name} (ID=${schoolId})`; + option.value = schoolId; + schoolSelect.append(option); + } + }) +} + +fetchSchools(); + +function init() { + const festivalCreateForm = document.getElementById("festivalCreateForm"); + festivalCreateForm.addEventListener("submit", createFestival); +} + +function createFestival(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const festivalData = { + name: formData.get("name"), + startDate: formData.get("festivalStartDate"), + endDate: formData.get("festivalEndDate"), + thumbnail: formData.get("thumbnail"), + schoolId: formData.get("school"), + }; + validateFestival(festivalData) + + fetch("/admin/api/festivals", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(festivalData) + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.json().then(data => { + throw new Error(data.message || "축제 생성에 실패하였습니다."); + }); + } + }) + .then(data => { + alert("축제가 성공적으로 생성되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +init(); diff --git a/backend/src/main/resources/static/js/admin/school/manage-school-detail.js b/backend/src/main/resources/static/js/admin/school/manage-school-detail.js new file mode 100644 index 000000000..a0027da54 --- /dev/null +++ b/backend/src/main/resources/static/js/admin/school/manage-school-detail.js @@ -0,0 +1,116 @@ +import {getResourceId} from "../../common/UrlParser.js"; + +const deleteConfirmModal = new bootstrap.Modal( + document.getElementById("deleteConfirmModal")); + +function fetchSchool() { + const idInput = document.getElementById("id"); + const fakeIdInput = document.getElementById("fakeId"); + const nameInput = document.getElementById("name"); + const domainInput = document.getElementById("domain"); + const updateBtn = document.getElementById("updateBtn"); + const deleteBtn = document.getElementById("deleteBtn"); + const schoolId = getResourceId(new URL(window.location.href)); + const errorModal = new bootstrap.Modal(document.getElementById("errorModal")); + + fetch(`/schools/${schoolId}`).then(res => { + if (!res.ok) { + nameInput.setAttribute("disabled", ""); + domainInput.setAttribute("disabled", ""); + updateBtn.setAttribute("disabled", ""); + deleteBtn.setAttribute("disabled", ""); + return res.json().then(data => { + throw new Error(data.message || data.detail) + }) + } + return res.json(); + }).then(school => { + idInput.value = school.id; + fakeIdInput.value = school.id; + nameInput.value = school.name; + domainInput.value = school.domain; + }).catch(error => { + const errorModalBody = document.getElementById("errorModalBody"); + errorModalBody.textContent = error.message; + errorModal.show(); + }) +} + +fetchSchool(); + +function init() { + const schoolUpdateForm = document.getElementById("schoolUpdateForm"); + const deleteBtn = document.getElementById("deleteBtn"); + const actualDeleteBtn = document.getElementById("actualDeleteBtn"); + + schoolUpdateForm.addEventListener("submit", updateSchool); + deleteBtn.addEventListener("click", openDeleteConfirmModal); + actualDeleteBtn.addEventListener("click", deleteSchool) +} + +function updateSchool(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const schoolData = { + name: formData.get("name"), + domain: formData.get("domain"), + }; + const schoolId = formData.get("id"); + fetch(`/admin/api/schools/${schoolId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(schoolData) + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "학교 수정에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("학교가 성공적으로 수정되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +init(); + +function openDeleteConfirmModal() { + deleteConfirmModal.show(); +} + +function deleteSchool() { + const idInput = document.getElementById("id"); + const schoolId = idInput.value; + fetch(`/admin/api/schools/${schoolId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + } + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "학교 삭제에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("학교가 성공적으로 삭제되었습니다!"); + location.replace("/admin/schools"); + }) + .catch(error => { + deleteConfirmModal.hide(); + alert(error.message); + }); +} diff --git a/backend/src/main/resources/static/js/admin/school/manage-school.js b/backend/src/main/resources/static/js/admin/school/manage-school.js new file mode 100644 index 000000000..be0cd897f --- /dev/null +++ b/backend/src/main/resources/static/js/admin/school/manage-school.js @@ -0,0 +1,82 @@ +function fetchSchools() { + const schoolGrid = document.getElementById("schoolGrid"); + + fetch("/schools").then(res => { + if (!res.ok) { + throw new Error("서버에 연결할 수 없습니다.") + } + return res.json(); + }).then(data => { + const schools = data.schools; + for (const school of schools) { + const row = document.createElement("div"); + row.classList.add("row", "align-items-center", "gx-0", "py-1", + "border-top"); + + const idColumn = document.createElement("div"); + idColumn.classList.add("col-2"); + idColumn.textContent = school.id; + + const nameColumn = document.createElement("div"); + nameColumn.classList.add("col-4"); + nameColumn.textContent = school.name; + + const domainColumn = document.createElement("div"); + domainColumn.classList.add("col-4"); + domainColumn.textContent = school.domain; + + const buttonColumn = document.createElement("div"); + buttonColumn.classList.add("col-2") + const button = document.createElement("a"); + button.classList.add("btn", "btn-primary"); + button.setAttribute("href", `schools/${school.id}`); + button.textContent = "편집"; + buttonColumn.append(button); + + row.append(idColumn, nameColumn, domainColumn, buttonColumn); + schoolGrid.append(row); + } + }) +} + +fetchSchools(); + +function init() { + const schoolCreateForm = document.getElementById("schoolCreateForm"); + schoolCreateForm.addEventListener("submit", createSchool); +} + +function createSchool(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const schoolData = { + name: formData.get("name"), + domain: formData.get("domain"), + }; + + fetch("/admin/api/schools", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(schoolData) + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.json().then(data => { + throw new Error(data.message || "학교 생성에 실패하였습니다."); + }); + } + }) + .then(data => { + alert("학교가 성공적으로 생성되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +init(); diff --git a/backend/src/main/resources/static/js/admin/signup.js b/backend/src/main/resources/static/js/admin/signup.js index ece7b9db4..34a391911 100644 --- a/backend/src/main/resources/static/js/admin/signup.js +++ b/backend/src/main/resources/static/js/admin/signup.js @@ -15,7 +15,7 @@ password: password, }; - fetch("/admin/signup", { + fetch("/admin/api/signup", { method: "POST", headers: { "Content-Type": "application/json" diff --git a/backend/src/main/resources/static/js/admin/stage/manage-stage-detail.js b/backend/src/main/resources/static/js/admin/stage/manage-stage-detail.js new file mode 100644 index 000000000..6a7c41814 --- /dev/null +++ b/backend/src/main/resources/static/js/admin/stage/manage-stage-detail.js @@ -0,0 +1,227 @@ +import {getResourceId} from "../../common/UrlParser.js"; + +const deleteConfirmModal = new bootstrap.Modal( + document.getElementById("deleteConfirmModal")); + +function fetchStage() { + const idInput = document.getElementById("id"); + const fakeIdInput = document.getElementById("fakeId"); + const festivalIdInput = document.getElementById("festivalId"); + const fakeFestivalIdInput = document.getElementById("fakeFestivalId"); + const startTimeInput = document.getElementById("startTime"); + const ticketOpenTime = document.getElementById("ticketOpenTime"); + const lineUpInput = document.getElementById("lineUp"); + const stageId = getResourceId(new URL(window.location.href)); + const errorModal = new bootstrap.Modal(document.getElementById("errorModal")); + const returnLink = document.getElementById("returnLink"); + + fetch(`/stages/${stageId}`).then(res => { + if (!res.ok) { + startTimeInput.setAttribute("disabled", ""); + ticketOpenTime.setAttribute("disabled", ""); + lineUpInput.setAttribute("disabled", ""); + return res.json().then(data => { + throw new Error(data.message || data.detail) + }) + } + return res.json(); + }).then(stage => { + idInput.value = stage.id; + fakeIdInput.value = stage.id; + festivalIdInput.value = stage.festivalId; + fakeFestivalIdInput.value = stage.festivalId; + startTimeInput.value = stage.startTime; + ticketOpenTime.value = stage.ticketOpenTime; + lineUpInput.value = stage.lineUp; + returnLink.setAttribute("href", `/admin/festivals/${stage.festivalId}`) + }).catch(error => { + const errorModalBody = document.getElementById("errorModalBody"); + errorModalBody.textContent = error.message; + errorModal.show(); + }) +} + +fetchStage(); + +function fetchTickets() { + const ticketGrid = document.getElementById("ticketGrid"); + const stageId = getResourceId(new URL(window.location.href)); + fetch(`/stages/${stageId}/tickets`).then(res => { + if (!res.ok) { + throw new Error("서버에 연결할 수 없습니다.") + } + return res.json(); + }).then(data => { + const tickets = data.tickets; + for (const ticket of tickets) { + const row = document.createElement("div"); + row.classList.add("row", "align-items-center", "gx-0", "py-1", + "border-top"); + + const idColumn = document.createElement("div"); + idColumn.classList.add("col-1"); + idColumn.textContent = ticket.id; + + const ticketTypeColumn = document.createElement("div"); + ticketTypeColumn.classList.add("col-3"); + ticketTypeColumn.textContent = ticket.ticketType; + + const totalAmountColumn = document.createElement("div"); + totalAmountColumn.classList.add("col-3"); + totalAmountColumn.textContent = ticket.totalAmount; + + const remainAmountColumn = document.createElement("div"); + remainAmountColumn.classList.add("col-3"); + remainAmountColumn.textContent = ticket.remainAmount; + + const buttonColumn = document.createElement("div"); + buttonColumn.classList.add("col-2") + const button = document.createElement("a"); + button.classList.add("btn", "btn-primary"); + button.setAttribute("href", `/admin/tickets/${ticket.id}`); + button.textContent = "편집"; + buttonColumn.append(button); + + row.append(idColumn, ticketTypeColumn, totalAmountColumn, + remainAmountColumn, buttonColumn); + ticketGrid.append(row); + } + }) +} + +fetchTickets(); + +function init() { + const schoolUpdateForm = document.getElementById("schoolUpdateForm"); + const ticketCreateForm = document.getElementById("ticketCreateForm"); + const deleteBtn = document.getElementById("deleteBtn"); + const actualDeleteBtn = document.getElementById("actualDeleteBtn"); + + schoolUpdateForm.addEventListener("submit", updateStage); + deleteBtn.addEventListener("click", openDeleteConfirmModal); + actualDeleteBtn.addEventListener("click", deleteStage) + ticketCreateForm.addEventListener("submit", createTicket) +} + +function updateStage(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const stageData = { + startTime: formData.get("startTime"), + ticketOpenTime: formData.get("ticketOpenTime"), + lineUp: formData.get("lineUp"), + }; + const stageId = formData.get("id"); + fetch(`/admin/api/stages/${stageId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(stageData) + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "공연 수정에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("공연이 성공적으로 수정되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +function openDeleteConfirmModal() { + deleteConfirmModal.show(); +} + +function deleteStage() { + const stageId = document.getElementById("id").value; + const festivalId = document.getElementById("festivalId").value; + fetch(`/admin/api/stages/${stageId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + } + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "공연 삭제에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("공연이 성공적으로 삭제되었습니다!"); + location.replace(`/admin/festivals/${festivalId}`); + }) + .catch(error => { + deleteConfirmModal.hide(); + alert(error.message); + }); +} + +function createTicket(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const ticketData = { + stageId: document.getElementById("id").value, + ticketType: formData.get("ticketType"), + amount: formData.get("amount"), + entryTime: formData.get("entryTime"), + }; + validateTicket(ticketData); + + fetch("/admin/api/tickets", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(ticketData) + }) + .then(response => { + if (response.ok) { + return response; + } else { + return response.json().then(data => { + throw new Error(data.message || "티켓 생성에 실패하였습니다."); + }); + } + }) + .then(() => { + alert("티켓이 성공적으로 생성되었습니다!"); + location.reload(); + }) + .catch(error => { + alert(error.message); + }); +} + +function validateTicket(ticketData) { + let hasError = false; + if (ticketData.amount <= 0) { + document.getElementById("amount").classList.add("is-invalid"); + document.getElementById("amount-feedback") + .textContent = "수량은 0보다 많아야 합니다." + hasError = true; + } + if (ticketData.amount > 100000) { + document.getElementById("amount").classList.add("is-invalid"); + document.getElementById("amount-feedback") + .textContent = "너무 많은 수량은 생성할 수 없습니다." + hasError = true; + } + if (hasError) { + throw new Error("검증이 실패하였습니다."); + } +} + +init(); diff --git a/backend/src/main/resources/static/js/common/UrlParser.js b/backend/src/main/resources/static/js/common/UrlParser.js new file mode 100644 index 000000000..376f075ee --- /dev/null +++ b/backend/src/main/resources/static/js/common/UrlParser.js @@ -0,0 +1,5 @@ +export function getResourceId(url) { + const pathname = url.pathname; + const parts = pathname.split("/"); + return parts[parts.length - 1]; +} diff --git a/backend/src/main/resources/static/js/login.js b/backend/src/main/resources/static/js/login.js index ec127877b..6f4623be7 100644 --- a/backend/src/main/resources/static/js/login.js +++ b/backend/src/main/resources/static/js/login.js @@ -7,7 +7,7 @@ password: formData.get("password"), }; - fetch("/admin/login", { + fetch("/admin/api/login", { method: "POST", headers: { "Content-Type": "application/json" diff --git a/backend/src/main/resources/templates/admin/admin-page.html b/backend/src/main/resources/templates/admin/admin-page.html index 7ab361930..79b793b98 100644 --- a/backend/src/main/resources/templates/admin/admin-page.html +++ b/backend/src/main/resources/templates/admin/admin-page.html @@ -1,90 +1,97 @@  - + + 어드민 페이지 - +
-
-

학교 생성 요청

-
- -
- - -
- - -
-
- -
-

축제 생성 요청

-
- -
- - -
- - -
- - -
- - -
- - -
-
- -
-

공연 생성 요청

-
- -
- - -
- - -
- - -
- - -
+
+

어드민 페이지

+
+ +
+
+
+

축제 관리

+ 이동 +
+
+
+ +
+
+
+

학교 관리

+ 이동 +
+
+
+ +
+
+
+

어드민 계정 생성

+ 이동 +
+
+
+ +
+
+
+

관리자 계정 관리(TODO)

+ 이동 +
+
+
+
+
+
+

서버 버전 조회

+ +
+
+
+ +
+
+
+

INFO 로그 생성

+ +
+
+
+ +
+
+
+

WARN 로그 생성

+ +
+
+
+ +
+
+
+

ERROR 로그 생성

+ +
+
+
-
-

티켓 생성 요청

-
- -
- - -
- - -
- - -
- - -
-
- - + + diff --git a/backend/src/main/resources/templates/admin/festival/manage-festival-detail.html b/backend/src/main/resources/templates/admin/festival/manage-festival-detail.html new file mode 100644 index 000000000..ca4e1ea26 --- /dev/null +++ b/backend/src/main/resources/templates/admin/festival/manage-festival-detail.html @@ -0,0 +1,154 @@ + + + + + + 어드민 페이지 - 축제 세부 관리 + + + +
+ +
+
+
+

축제 수정

+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+

공연 등록

+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+
+
+

공연 목록

+
+
+
+
+ ID +
+
+ 시작시간 +
+
+ 티켓 오픈 시간 +
+
+ 라인업 +
+
+
+
+ + + + + + diff --git a/backend/src/main/resources/templates/admin/festival/manage-festival.html b/backend/src/main/resources/templates/admin/festival/manage-festival.html new file mode 100644 index 000000000..759f9462c --- /dev/null +++ b/backend/src/main/resources/templates/admin/festival/manage-festival.html @@ -0,0 +1,84 @@ + + + + + + 어드민 페이지 - 축제 관리 + + + +
+ +
+

축제 생성

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+

축제 목록

+
+
+
+
+ ID +
+
+ 학교 ID +
+
+ 이름 +
+
+ 썸네일 +
+
+ 시작일 +
+
+ 종료일 +
+
+
+
+ + + + diff --git a/backend/src/main/resources/templates/admin/login.html b/backend/src/main/resources/templates/admin/login.html index 8f8deb745..28ee7be87 100644 --- a/backend/src/main/resources/templates/admin/login.html +++ b/backend/src/main/resources/templates/admin/login.html @@ -6,7 +6,7 @@