diff --git a/build.gradle b/build.gradle index cb284ad..24e3c18 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,9 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation "org.springframework.boot:spring-boot-starter-mail" + //swagger + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' + } tasks.named('test') { diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/file/service/FileService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/file/service/FileService.java index 01ad6dc..55cc689 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/file/service/FileService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/file/service/FileService.java @@ -202,48 +202,6 @@ public InputStream openObject(String key) { return obj.getObjectContent(); // 반드시 호출 측에서 close } -// public ResponseEntity viewImage(String fileId,String type) throws IOException { -// FileTargetType fileType = FileTargetType.fromType(type); -// FileAttachment file = fileAttachmentRepository.findByFileIdAndFiletype(fileId,fileType) -// .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다.")); -// Path filePath = Paths.get(file.getPath()).resolve(file.getStoredName()); -// Resource resource = new UrlResource(filePath.toUri()); -// if (!resource.exists() || !resource.isReadable()) { -// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "파일을 읽을 수 없습니다."); -// } -// -// String contentType = Files.probeContentType(filePath); -// if (contentType == null) { -// contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; -// } -// return ResponseEntity.ok() -// .contentType(MediaType.parseMediaType(contentType)) -// .body(resource); -// } -// -// public ResponseEntity viewPdf(String fileId, String type) throws IOException { -// FileTargetType fileType = FileTargetType.fromType(type); -// FileAttachment file = fileAttachmentRepository.findByFileIdAndFiletype(fileId, fileType) -// .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다.")); -// -// Path filePath = Paths.get(file.getPath()).resolve(file.getStoredName()); -// Resource resource = new UrlResource(filePath.toUri()); -// -// if (!resource.exists() || !resource.isReadable()) { -// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "파일을 읽을 수 없습니다."); -// } -// -// String contentType = Files.probeContentType(filePath); -// if (contentType == null) { -// contentType = MediaType.APPLICATION_PDF_VALUE; // 기본값을 PDF로 설정 -// } -// -// return ResponseEntity.ok() -// .contentType(MediaType.parseMediaType(contentType)) -// .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"") -// .body(resource); -// } - @Transactional public void deleteFile(FileAttachment fileAttachment){ try{ diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberProfileRestController.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberProfileRestController.java index 43f0c37..6995be3 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberProfileRestController.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberProfileRestController.java @@ -5,7 +5,9 @@ import com.cooperation.project.cooperationcenter.domain.member.dto.UpdatePasswordDto; import com.cooperation.project.cooperationcenter.domain.member.service.MemberService; import com.cooperation.project.cooperationcenter.domain.member.service.ProfileService; +import com.cooperation.project.cooperationcenter.global.exception.BaseException; import com.cooperation.project.cooperationcenter.global.exception.BaseResponse; +import com.cooperation.project.cooperationcenter.global.exception.codes.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -23,17 +25,32 @@ public class MemberProfileRestController { @PatchMapping("/member") public BaseResponse updateMemberInfo(@RequestBody Profile.MemberDto request, @AuthenticationPrincipal MemberDetails memberDetails){ - log.info("request:{}",request.toString()); - profileService.updateMember(request,memberDetails); - return BaseResponse.onSuccess("success"); + try{ + profileService.updateMember(request,memberDetails); + return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + log.warn(e.getCode().toString()); + return BaseResponse.onFailure(e.getCode(),null); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } @PatchMapping("/agency") public BaseResponse updateAgencyInfo(@RequestBody Profile.MemberDto request, @AuthenticationPrincipal MemberDetails memberDetails){ log.info("request:{}",request.toString()); - profileService.updateAgency(request,memberDetails); - return BaseResponse.onSuccess("success"); + try{ + profileService.updateAgency(request,memberDetails); + return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + log.warn(e.getCode().toString()); + return BaseResponse.onFailure(e.getCode(),null); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } @PatchMapping("/businessCert") @@ -41,8 +58,16 @@ public BaseResponse updateBusinessCertificate( @RequestPart(name = "businessCertificate", required = false) MultipartFile file , @AuthenticationPrincipal MemberDetails memberDetails ){ - profileService.updateBussinessCert(file,memberDetails); - return BaseResponse.onSuccess("success"); + try{ + profileService.updateBussinessCert(file,memberDetails); + return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + log.warn(e.getCode().toString()); + return BaseResponse.onFailure(e.getCode(),null); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } @PatchMapping("/agencyPicture") @@ -50,7 +75,15 @@ public BaseResponse updateAgencyPicture( @RequestPart(name = "agencyPicture", required = false) MultipartFile file , @AuthenticationPrincipal MemberDetails memberDetails ){ - profileService.updateAgencyPicture(file,memberDetails); - return BaseResponse.onSuccess("success"); + try{ + profileService.updateAgencyPicture(file,memberDetails); + return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + log.warn(e.getCode().toString()); + return BaseResponse.onFailure(e.getCode(),null); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberRestController.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberRestController.java index d74f292..c69598a 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberRestController.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/controller/homepage/MemberRestController.java @@ -4,6 +4,7 @@ import com.cooperation.project.cooperationcenter.domain.member.dto.MemberRequest; import com.cooperation.project.cooperationcenter.domain.member.dto.UpdatePasswordDto; import com.cooperation.project.cooperationcenter.domain.member.service.MemberService; +import com.cooperation.project.cooperationcenter.global.exception.BaseException; import com.cooperation.project.cooperationcenter.global.exception.BaseResponse; import com.cooperation.project.cooperationcenter.global.exception.codes.ErrorCode; import com.fasterxml.jackson.core.JsonProcessingException; @@ -36,30 +37,52 @@ public BaseResponse signup( try{ memberService.signup(data,agencyPicture,businessCertificate); return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + return BaseResponse.onFailure(e.getCode(),null); }catch (Exception e){ log.warn(e.getMessage()); - return BaseResponse.onFailure(ErrorCode.BAD_REQUEST,null); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); } } @GetMapping("/check-id") public BaseResponse checkDuplicateId(@RequestParam String username) { - boolean isDuplicate = memberService.isUsernameTaken(username); - log.info("response:{}",isDuplicate); - return BaseResponse.onSuccess(isDuplicate); + try{ + return BaseResponse.onSuccess(memberService.isUsernameTaken(username)); + }catch (BaseException e){ + return BaseResponse.onFailure(e.getCode(),false); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } @PostMapping("/login") public BaseResponse login(@RequestBody MemberRequest.LoginDto requestDto, HttpServletResponse response,HttpServletRequest request){ + try{ memberService.login(requestDto,response,request); log.info("loginSuccess"); return BaseResponse.onSuccess("success"); + }catch (BaseException e){ + return BaseResponse.onFailure(e.getCode(),false); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } + } @PostMapping("/logout") public BaseResponse userLogout(HttpServletRequest request ,HttpServletResponse response){ - memberService.logout(request,response); - return BaseResponse.onSuccess("log out success"); + try { + memberService.logout(request, response); + return BaseResponse.onSuccess("log out success"); + }catch (BaseException e){ + return BaseResponse.onFailure(e.getCode(),false); + }catch (Exception e){ + log.warn(e.getMessage()); + return BaseResponse.onFailure("ERROR",e.getMessage().toString(),false); + } } @PostMapping("/refresh") diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberAdminService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberAdminService.java deleted file mode 100644 index 2a7ed32..0000000 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberAdminService.java +++ /dev/null @@ -1,97 +0,0 @@ -//package com.cooperation.project.cooperationcenter.domain.member.service; -// -// -//import com.cooperation.project.cooperationcenter.domain.member.dto.MemberAdminStatsDto; -//import com.cooperation.project.cooperationcenter.domain.member.model.UserStatus; -//import com.cooperation.project.cooperationcenter.domain.member.repository.MemberRepository; -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import org.springframework.stereotype.Service; -// -//import java.time.LocalDate; -//import java.time.YearMonth; -// -//@Service -//@RequiredArgsConstructor -//@Slf4j -//public class MemberAdminService { -// -//// public MemberResponse.LoginDto login(MemberRequest.LoginDto request, HttpServletResponse response) { -//// Member member = findMemberByEmail(request.email()); -//// if(member == null) { -//// log.warn("해당 이메일을 가진 멤버가 존재하지 않음"); -//// throw new BaseException(ErrorCode.EMAIL_NOT_FOUND); -//// } -//// -//// if(!member.getPassword().equals(request.password())){ -//// log.warn("로그인 실패"); -//// throw new BaseException(ErrorCode.PASSWORD_ERROR); -//// } -//// -//// if(!member.getRole().equals(Member.Role.ADMIN)){ -//// throw new BaseException(ErrorCode.MEMBER_NOT_ADMIN); -//// } -//// -//// TokenResponse tokenResponse = getTokenResponse(response,member); -//// memberCookieService.addTokenCookies(response,tokenResponse); -//// return MemberResponse.LoginDto.from(member); -//// } -//// -//// public Member findMemberByEmail(String email) { -//// try{ -//// Member member = memberRepository.findMemberByEmail(email).orElseThrow( -//// () -> new BaseException(ErrorCode.MEMBER_NOT_FOUND) -//// ); -//// return member; -//// }catch(Exception e){ -//// return null; -//// } -//// } -//// -//// public MemberResponse.LoginDto logout(HttpServletResponse response, HttpSession session, MemberDetails memberDetails, HttpServletRequest request){ -//// Member member = getMember(memberDetails.getUsername()); -//// //fixme 토큰 response가져와서 그걸 바꿔야함. -//// -//// AccessToken accessToken = AccessToken.of(jwtProvider.resolvAccesseToken(request)); -//// RefreshToken refreshToken = RefreshToken.of(jwtProvider.resolveRefreshToken(request)); -//// log.info("refresh:{}",refreshToken.token()); -//// memberCookieService.deleteCookie(response,TokenResponse.of(accessToken,refreshToken)); -//// log.info("cookie삭제"); -//// session.invalidate(); -//// log.info("세션삭제"); -//// return MemberResponse.LoginDto.from(member); -//// } -//// -//// @NotNull -//// private TokenResponse getTokenResponse(HttpServletResponse response, Member member) { -//// -//// AccessToken accessToken = jwtProvider.generateAccessToken(member); -//// RefreshToken refreshToken = jwtProvider.generateRefreshToken(member); -//// TokenResponse tokenResponse = TokenResponse.of(accessToken, refreshToken); -//// return tokenResponse; -//// } -//// -//// public Member getMember(String email){ -//// try { -//// return memberRepository.findMemberByEmail(email) -//// .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND)); -//// } catch (BaseException e){ -//// log.warn("멤버 조회 실패: {}", e.getMessage()); -//// return null; -//// } catch (Exception e){ -//// log.error("알 수 없는 에러 발생: {}", e.getMessage(), e); -//// return null; -//// } -//// } -//// -//// @Transactional -//// public void acceptedMember(String email){ -//// Member member = memberRepository.findMemberByEmail(email) -//// .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 이메일입니다.")); -//// member.accept(); -//// memberRepository.save(member); -//// -//// Agency agency = Agency.fromMember(member); -//// agencyRepository.save(agency); -//// } -//} diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberService.java index 6e987a9..7847bae 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/MemberService.java @@ -183,13 +183,8 @@ public Member getMember(String email){ } public boolean isUsernameTaken(String username){ - try{ log.info("username:{}",username); return memberRepository.existsMemberByEmail(username); - }catch (Exception e){ - log.warn(e.getMessage()); - return false; - } } @Transactional diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/ProfileService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/ProfileService.java index b551fdb..5755987 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/ProfileService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/member/service/ProfileService.java @@ -40,16 +40,8 @@ public class ProfileService { private final FileService fileService; public Member getMember(String email){ - try { - return memberRepository.findMemberByEmail(email) - .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND)); - } catch (BaseException e){ - log.warn("멤버 조회 실패: {}", e.getMessage()); - return null; - } catch (Exception e){ - log.error("알 수 없는 에러 발생: {}", e.getMessage(), e); - return null; - } + return memberRepository.findMemberByEmail(email) + .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND)); } public Profile.ProfileDto getProfileDto(MemberDetails memberDetails, Pageable pageable){ diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/school/repository/SchoolPostRepository.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/school/repository/SchoolPostRepository.java index 57b3acc..6d7d77e 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/school/repository/SchoolPostRepository.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/school/repository/SchoolPostRepository.java @@ -25,13 +25,17 @@ public interface SchoolPostRepository extends JpaRepository { void incrementViewCount(@Param("postId") Long postId); @Query(""" - SELECT p FROM SchoolPost p - WHERE p.schoolBoard.id = :boardId - ORDER BY - CASE WHEN p.type = com.cooperation.project.cooperationcenter.domain.school.dto.PostType.NOTICE THEN 0 ELSE 2 END, - p.createdAt DESC - """) - Page findPostsByBoardOrderByNoticeFirst(@Param("boardId") Long boardId, Pageable pageable); + SELECT p FROM SchoolPost p + WHERE p.schoolBoard.id = :boardId + AND p.status = com.cooperation.project.cooperationcenter.domain.school.dto.PostStatus.PUBLISHED + ORDER BY + CASE WHEN p.type = com.cooperation.project.cooperationcenter.domain.school.dto.PostType.NOTICE THEN 0 ELSE 2 END, + p.createdAt DESC +""") + Page findPostsByBoardOrderByNoticeFirst( + @Param("boardId") Long boardId, + Pageable pageable + ); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/controller/homepage/SurveyRestController.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/controller/homepage/SurveyRestController.java index 4ba2c0b..4860d21 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/controller/homepage/SurveyRestController.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/controller/homepage/SurveyRestController.java @@ -1,10 +1,8 @@ package com.cooperation.project.cooperationcenter.domain.survey.controller.homepage; import com.cooperation.project.cooperationcenter.domain.member.dto.MemberDetails; -import com.cooperation.project.cooperationcenter.domain.student.dto.StudentRequest; import com.cooperation.project.cooperationcenter.domain.survey.dto.*; import com.cooperation.project.cooperationcenter.domain.survey.model.Survey; -import com.cooperation.project.cooperationcenter.domain.survey.model.SurveyFolder; import com.cooperation.project.cooperationcenter.domain.survey.service.homepage.*; import com.cooperation.project.cooperationcenter.global.exception.BaseResponse; import com.fasterxml.jackson.core.JsonProcessingException; @@ -13,8 +11,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.Resource; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; @@ -22,6 +18,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.io.IOException; import java.util.List; @@ -106,6 +103,13 @@ public ResponseEntity receiveSurveyAnswer( return ResponseEntity.ok().build(); } + @GetMapping("/admin/answer") + public BaseResponse getAllAnswerLog(){ + List result = surveyLogService.getAllAnswerLog(); + log.info("result : {}",result.toString()); + return BaseResponse.onSuccess(result); + } + @GetMapping("/admin/answer/{surveyId}") public BaseResponse getAnswerLog(@PathVariable String surveyId){ AnswerResponse.AnswerDto result = surveyLogService.getAnswerLog(surveyId); @@ -114,23 +118,23 @@ public BaseResponse getAnswerLog(@PathVariable String surveyId){ } @PostMapping("/admin/log/csv") - public ResponseEntity extractCsv(@RequestBody LogCsv.RequestDto request){ + public ResponseEntity extractCsv(@RequestBody LogCsv.RequestDto request){ log.info("[enter extract csv]"); return surveyLogService.extractCsv(request); } @PostMapping("/admin/log/{surveyId}") - public ResponseEntity extractCsv(@PathVariable String surveyId){ + public ResponseEntity extractCsv(@PathVariable String surveyId){ log.info("extracy all csv..."); return surveyLogService.extractAllCsv(surveyId); } @PostMapping("/admin/log/file/student/{surveyId}") - public ResponseEntity extractFileStudent(@PathVariable String surveyId){ + public ResponseEntity extractFileStudent(@PathVariable String surveyId){ return surveyLogService.extractFileStudent(surveyId); } @PostMapping("/admin/log/file/survey/{surveyId}") - public ResponseEntity extractFileSurvey(@PathVariable String surveyId){ + public ResponseEntity extractFileSurvey(@PathVariable String surveyId){ return surveyLogService.extractFileSurvey(surveyId); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/AnswerResponse.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/AnswerResponse.java index 183f000..bc2b30d 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/AnswerResponse.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/AnswerResponse.java @@ -110,8 +110,8 @@ public record LogDto( String submitTime, Long spendTime, String finishStatus, - String logId - + String logId, + String surveyName ){ public static LogDto from(SurveyLog surveyLog){ long diffInSeconds = Duration.between(surveyLog.getStartTime(), surveyLog.getCreatedAt()).getSeconds(); @@ -121,7 +121,8 @@ public static LogDto from(SurveyLog surveyLog){ surveyLog.getCreatedAt().toString(), diffInSeconds, "finish", - surveyLog.getSurveyLogId() + surveyLog.getSurveyLogId(), + surveyLog.getSurvey().getSurveyTitle() ); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/SurveyFolderDto.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/SurveyFolderDto.java index 0415f92..5e4919b 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/SurveyFolderDto.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/dto/SurveyFolderDto.java @@ -10,6 +10,7 @@ public record SurveyFolderDto( String folderId, String displayName, String storedName, + int surveyCnt, LocalDateTime createdAt ) { public static SurveyFolderDto from(SurveyFolder surveyFolder) { @@ -17,6 +18,7 @@ public static SurveyFolderDto from(SurveyFolder surveyFolder) { surveyFolder.getFolderId(), surveyFolder.getDisplayName(), surveyFolder.getStoredName(), + surveyFolder.getSurveys().size(), surveyFolder.getCreatedAt() ); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/repository/SurveyLogRepository.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/repository/SurveyLogRepository.java index 257c73e..682c321 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/repository/SurveyLogRepository.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/repository/SurveyLogRepository.java @@ -16,4 +16,5 @@ public interface SurveyLogRepository extends JpaRepository { SurveyLog findSurveyLogBySurveyLogId(String logId); Page findSurveysLogByMember(Member member, Pageable pageable); + List findTop7ByOrderByCreatedAtDesc(); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyAnswerService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyAnswerService.java index 367b370..65d42f0 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyAnswerService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyAnswerService.java @@ -61,7 +61,7 @@ public void answerSurvey(String data, HttpServletRequest request, MemberDetails } Survey survey = surveyFindService.getSurveyFromId(requestDto.surveyId()); -// if(checkDate(survey)) throw new BaseException(ErrorCode.SURVEY_DATE_NOT_VALID); + if(checkDate(survey)) throw new BaseException(ErrorCode.SURVEY_DATE_NOT_VALID); survey.setParticipantCount(); Member member = memberRepository.findMemberByEmail(memberDetails.getUsername()).get(); @@ -169,11 +169,15 @@ private FileAttachment saveFile(AnswerRequest.AnswerDto answer,MultipartHttpServ if (answer.answer() instanceof String str) key = answer.answer().toString(); MultipartFile file = multipartRequest.getFile(key); + double mb = file.getSize() / 1024.0 / 1024.0; + if (file == null) { log.warn("❌ {} 필드 없음", key); } else if (file.isEmpty()) { log.warn("❌ {} 는 비어 있음", key); - } else { + } else if(mb>20){ + throw new BaseException(ErrorCode.FILE_SIZE_ERROR); + }else { log.info("✅ {} 수신 성공: {}", key, file.getOriginalFilename()); //key예시 file-0 image-1 String type = key.split("-")[0]; diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyFindService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyFindService.java index 8ec046a..4dcbf04 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyFindService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyFindService.java @@ -198,6 +198,15 @@ public Page getSurveyFromCondition(Pageable pageable, SurveyRequest.LogF } } + public List findAllSurveyLog(){ + try{ + return surveyLogRepository.findTop7ByOrderByCreatedAtDesc(); + }catch (Exception e){ + log.warn(e.getMessage()); + return null; + } + } + public SurveyLog getSurveyLog(String logId){ try{ return surveyLogRepository.findSurveyLogBySurveyLogId(logId); diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyLogService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyLogService.java index 2a952c4..1951086 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyLogService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveyLogService.java @@ -7,24 +7,22 @@ import com.cooperation.project.cooperationcenter.domain.survey.dto.LogCsv; import com.cooperation.project.cooperationcenter.domain.survey.model.*; import com.cooperation.project.cooperationcenter.domain.survey.repository.AnswerRepository; +import com.cooperation.project.cooperationcenter.global.exception.BaseException; +import com.cooperation.project.cooperationcenter.global.exception.codes.ErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -51,6 +49,13 @@ public AnswerResponse.AnswerPagedDto getAnswerLog(String surveyId, Pageable page return AnswerResponse.AnswerPagedDto.from(survey,logs); } + public List getAllAnswerLog(){ + List surveyLog = surveyFindService.findAllSurveyLog(); + return AnswerResponse.LogDto.from(surveyLog); + + } + + public AnswerResponse.AnswerDto getAnswerLog(String surveyId){ Survey survey = surveyFindService.getSurveyFromId(surveyId); List surveyLog = surveyFindService.getSurveyLogs(survey); @@ -75,233 +80,255 @@ public AnswerResponse.AnswerLogDto getAnswerLogDetail(String logId){ return AnswerResponse.AnswerLogDto.from(survey,surveyLog,answers); } - public ResponseEntity extractCsv(LogCsv.RequestDto request){ - StringBuilder csvBuilder = new StringBuilder(); - final String UTF8_BOM = "\uFEFF"; // ← 이게 핵심! - csvBuilder.append(UTF8_BOM); + public ResponseEntity extractCsv(LogCsv.RequestDto request){ + var logs = surveyFindService.getSurveyLogs(request.logIds()); + if (logs == null || logs.isEmpty()) throw new BaseException(ErrorCode.BAD_REQUEST); - List logs = surveyFindService.getSurveyLogs(request.logIds()); - if(logs == null){ - //fixme 확인해야함. - log.warn("log is null"); - } - Survey survey = Objects.requireNonNull(logs).get(0).getSurvey(); + Survey survey = logs.get(0).getSurvey(); List questions = surveyFindService.getQuestions(survey); - List questionTexts = questions.stream() - .map(q -> toCsvSafe(q.getQuestion())) - .toList(); - String questionLine = "no,"+String.join(",", questionTexts); - questionLine+="\n"; - System.out.println(questionLine); - csvBuilder.append(questionLine); - - int i=1; - for(SurveyLog log : logs){ - List answers = surveyFindService.getAnswer(log); - - Map answerMap = new TreeMap<>(); - for (Answer answer : answers) { - answerMap.put(answer.getQuestionId(), answer); - } + StreamingResponseBody body = out -> { + // Excel UTF-8 BOM + out.write("\uFEFF".getBytes(StandardCharsets.UTF_8)); + var writer = new java.io.BufferedWriter(new java.io.OutputStreamWriter(out, StandardCharsets.UTF_8)); + + // 헤더: questionOrder 기준(escape) + writer.write("no,"); + writer.write(questions.stream().map(q -> toCsvSafe(q.getQuestion())).collect(Collectors.joining(","))); + writer.write("\n"); - int maxQuestionId = questions.size(); - - List answerTexts = new ArrayList<>(maxQuestionId); - for (int j = 1; j <= maxQuestionId; j++) { - Answer a = answerMap.get(j); - if (a == null) { - answerTexts.add(""); // 빈 칸 처리 - } else if (QuestionType.isFile(a.getAnswerType())) { - answerTexts.add("\"=HYPERLINK(\"\"" + origin + a.getAnswer().split("_")[0] + "\"\")\""); - } else if (QuestionType.checkType(a.getAnswerType())) { - answerTexts.add(toCsvSafe(surveyFindService.getAnswerFromMultiple(a))); - } else { - answerTexts.add(a.getAnswer()); + int row = 1; + for (SurveyLog log : logs) { + // N+1 방지: findService에서 answers를 fetch join으로 가져오게 하거나 여기서 배치 조회 + List answers = surveyFindService.getAnswer(log); + + // questionId -> Answer 매핑 + Map byQid = answers.stream() + .collect(Collectors.toMap(Answer::getQuestionRealId, a -> a, (a,b)->a)); + + List cells = new ArrayList<>(questions.size()+1); + cells.add(String.valueOf(row++)); + + // “질문 리스트 순서(questionOrder)”대로 셀 채우기 (size로 1..N 도는 방식 지양) + for (Question q : questions) { + Answer a = byQid.get(q.getQuestionId()); + if (a == null) { cells.add(""); continue; } + + if (QuestionType.isFile(a.getAnswerType())) { + String id = a.getAnswer().split("_")[0]; // TODO: 안전한 파싱으로 교체 권장 + // 하이퍼링크 수식 주입 시에도 CSV 인젝션 보호 필요 + cells.add("\"=HYPERLINK(\"\"" + origin + a.getAnswer().split("_")[0] + "\"\")\""); + } else if (QuestionType.checkType(a.getAnswerType())) { + cells.add(toCsvSafe(surveyFindService.getAnswerFromMultiple(a))); + } else { + cells.add(toCsvSafe(a.getAnswer())); + } } + writer.write(String.join(",", cells)); + writer.write("\n"); + writer.flush(); // 청크 단위로 흘려보내기 } - String AnswerLine = (i++)+","+String.join(",", answerTexts); - AnswerLine+="\n"; - System.out.println(AnswerLine); - csvBuilder.append(AnswerLine); - } - - byte[] csvBytes = csvBuilder.toString().getBytes(StandardCharsets.UTF_8); - ByteArrayResource resource = new ByteArrayResource(csvBytes); + writer.flush(); + }; - // 2. 헤더 설정 및 응답 return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=survey-logs.csv") - .contentType(MediaType.parseMediaType("text/csv")) - .contentLength(csvBytes.length) - .body(resource); + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition("survey-logs.csv")) + .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8")) + .body(body); } - public ResponseEntity extractAllCsv(String surveyId){ - StringBuilder csvBuilder = new StringBuilder(); - final String UTF8_BOM = "\uFEFF"; // ← 이게 핵심! - csvBuilder.append(UTF8_BOM); - Survey survey = surveyFindService.getSurveyFromId(surveyId); - List logs = surveyFindService.getSurveyLogs(survey); + private static String contentDisposition(String filename) { + // RFC 5987 방식 + 기본 filename 함께 제공 + String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8) + .replaceAll("\\+", "%20"); + return "attachment; filename=\"" + filename + "\"; filename*=UTF-8''" + encoded; + } - if(logs == null){ - //fixme 확인해야함. - log.warn("log is null"); + public ResponseEntity extractAllCsv(String surveyId){ + final String fileBaseUrl = origin; // 기존 origin 사용 + final Survey survey = surveyFindService.getSurveyFromId(surveyId); + final List logs = surveyFindService.getSurveyLogs(survey); + if (logs == null || logs.isEmpty()) { + throw new BaseException(ErrorCode.BAD_REQUEST); } - List questions = surveyFindService.getQuestions(survey); + final List questions = surveyFindService.getQuestions(survey); + + StreamingResponseBody body = out -> { + // 엑셀 호환 BOM + out.write("\uFEFF".getBytes(StandardCharsets.UTF_8)); + + try (BufferedWriter writer = + new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { + + // 1) 헤더 (질문 순서대로) + String header = "no," + questions.stream() + .map(q -> toCsvSafe(q.getQuestion())) + .collect(Collectors.joining(",")); + writer.write(header); + writer.write("\n"); + writer.flush(); + + // 2) 본문 + int row = 1; + for (SurveyLog log : logs) { + // N+1이면 surveyFindService.getAnswer(log) 쪽 fetch join/일괄조회 최적화 권장 + List answers = surveyFindService.getAnswer(log); + + // 질문ID(real) -> 답변 맵 (※ Answer에 getQuestionRealId가 없으면 getQuestionId로 대체) + Map byQid = new HashMap<>(); + for (Answer a : answers) { + String key = a.getQuestionRealId(); // 없으면 a.getQuestionId() + byQid.put(key, a); + } - List questionTexts = questions.stream() - .map(q -> toCsvSafe(q.getQuestion())) - .toList(); - String questionLine = "no,"+String.join(",", questionTexts); - questionLine+="\n"; - System.out.println(questionLine); - csvBuilder.append(questionLine); - - int i=1; - for(SurveyLog log : logs){ - List answers = surveyFindService.getAnswer(log); - - Map answerMap = new TreeMap<>(); - for (Answer answer : answers) { - answerMap.put(answer.getQuestionId(), answer); - } + List cells = new ArrayList<>(questions.size() + 1); + cells.add(String.valueOf(row++)); + + // 질문 리스트 순서(questionOrder)대로 채우기 + for (Question q : questions) { + Answer a = byQid.get(q.getQuestionId()); + if (a == null) { + cells.add(""); + continue; + } + + if (QuestionType.isFile(a.getAnswerType())) { + // 파일 셀은 의도적으로 HYPERLINK 수식 사용 + String fileKey = safeFirstToken(a.getAnswer()); // "id_rest" 형태 보호 + cells.add("\"=HYPERLINK(\"\"" + fileBaseUrl + fileKey + "\"\")\""); + } else if (QuestionType.checkType(a.getAnswerType())) { + cells.add(toCsvSafe(surveyFindService.getAnswerFromMultiple(a))); + } else { + // 일반 텍스트는 CSV 인젝션 방지 + CSV escape + cells.add(toCsvSafe(preventCsvInjection(a.getAnswer()))); + } + } - int maxQuestionId = questions.size(); - - List answerTexts = new ArrayList<>(maxQuestionId); - for (int j = 1; j <= maxQuestionId; j++) { - Answer a = answerMap.get(j); - if (a == null) { - answerTexts.add(""); // 빈 칸 처리 - } else if (QuestionType.isFile(a.getAnswerType())) { - answerTexts.add("\"=HYPERLINK(\"\"" + origin + a.getAnswer().split("_")[0] + "\"\")\""); - } else if (QuestionType.checkType(a.getAnswerType())) { - answerTexts.add(toCsvSafe(surveyFindService.getAnswerFromMultiple(a))); - } else { - answerTexts.add(a.getAnswer()); + writer.write(String.join(",", cells)); + writer.write("\n"); + writer.flush(); // 청크로 바로바로 전송 } + writer.flush(); } - String AnswerLine = (i++)+","+String.join(",", answerTexts); - AnswerLine+="\n"; - System.out.println(AnswerLine); - csvBuilder.append(AnswerLine); - } + }; - byte[] csvBytes = csvBuilder.toString().getBytes(StandardCharsets.UTF_8); - ByteArrayResource resource = new ByteArrayResource(csvBytes); + ContentDisposition cd = ContentDisposition.attachment() + .filename("survey-logs.csv", StandardCharsets.UTF_8) + .build(); - // 2. 헤더 설정 및 응답 return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=survey-logs.csv") - .contentType(MediaType.parseMediaType("text/csv")) - .contentLength(csvBytes.length) - .body(resource); + .header(HttpHeaders.CONTENT_DISPOSITION, cd.toString()) + .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8")) + .body(body); } - public ResponseEntity extractFileStudent(String surveyId){ - log.info("학생 폴더 출력 start..."); - Survey survey = surveyFindService.getSurveyFromId(surveyId); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(baos)) { - List logs = surveyFindService.getSurveyLogs(survey); - int logIndex = 1; - for (SurveyLog log : logs) { - String memberName = log.getMember().getMemberName(); - String logFolder = logIndex + "_" + memberName + "/"; - - List answers = log.getAnswers(); - - for (Answer answer : answers) { - System.out.println("teset: "+answer.getAnswerType()); - if(!(answer.getAnswerType().equals(QuestionType.FILE)||answer.getAnswerType().equals(QuestionType.IMAGE))) continue; - - FileAttachment file = answer.getSurveyFile(); - System.out.println("filename:"+file.getStoredName()); - - if(!ossService.isFileExist(file)){ - System.out.println("해당 파일 없음"); - continue; - } + private static String preventCsvInjection(String s) { + if (s == null || s.isEmpty()) return ""; + char c = s.charAt(0); + if (c == '=' || c == '+' || c == '-' || c == '@' || c == '\t') { + return "'" + s; + } + return s; + } + private static String safeFirstToken(String v) { + if (v == null || v.isBlank()) return ""; + int idx = v.indexOf('_'); + String token = (idx >= 0) ? v.substring(0, idx) : v; + return token.replaceAll("[\\r\\n\\t\\x00-\\x1F\\x7F]", ""); + } - String fileName = "Q" + answer.getQuestionId() + "_" + file.getOriginalName(); - String zipEntryName = logFolder + fileName; - zos.putNextEntry(new ZipEntry(zipEntryName)); + public ResponseEntity extractFileStudent(String surveyId){ + log.info("학생 폴더 출력 start..."); + Survey survey = surveyFindService.getSurveyFromId(surveyId); + List logs = surveyFindService.getSurveyLogs(survey); - try (OSSObject ossObject = ossService.getObject(file); - InputStream inputStream = ossObject.getObjectContent()) { - inputStream.transferTo(zos); + StreamingResponseBody body = out -> { + try (ZipOutputStream zos = new ZipOutputStream(out)) { + int idx = 1; + for (SurveyLog log : logs) { + String memberName = log.getMember() != null ? log.getMember().getMemberName() : "unknown"; + String folder = idx++ + "_" + memberName + "/"; + + // N+1 주의: answers를 fetch join으로 미리 가져오게 + for (Answer a : surveyFindService.getAnswer(log)) { + if (!(a.getAnswerType() == QuestionType.FILE || a.getAnswerType() == QuestionType.IMAGE)) continue; + + FileAttachment f = a.getSurveyFile(); + if (f == null) continue; + + if (!ossService.isFileExist(f)) { + System.out.println("file missing: "+f.getStoredName()); + continue; } + + String entryName = folder + "Q" + a.getQuestionId() + "_" + f.getOriginalName(); + zos.putNextEntry(new ZipEntry(entryName)); + try (var obj = ossService.getObject(f); InputStream in = obj.getObjectContent()) { + in.transferTo(zos); + } + zos.closeEntry(); } - zos.closeEntry(); } - logIndex++; + zos.finish(); } - zos.finish(); - - } catch (IOException e) { - log.warn(e.getMessage()); - throw new RuntimeException(e); - } - - byte[] zipBytes = baos.toByteArray(); - log.info("zip 생성 완료"); + }; return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + surveyId + "_logs.zip\"") + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition(surveyId + "_logs.zip")) .contentType(MediaType.APPLICATION_OCTET_STREAM) - .contentLength(zipBytes.length) - .body(zipBytes); + .body(body); } - public ResponseEntity extractFileSurvey(String surveyId){ + public ResponseEntity extractFileSurvey(String surveyId){ log.info("설문조사 폴더 출력 start..."); - Survey survey =surveyFindService.getSurveyFromId(surveyId);; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(baos)) { - - List questions = survey.getQuestions(); - - for (Question question : questions) { - if(!(question.getQuestionType().equals(QuestionType.FILE)||question.getQuestionType().equals(QuestionType.IMAGE))) continue; - List answers = answerRepository.findAnswerByQuestionRealId(question.getQuestionId()); - List files = answers.stream().map(Answer::getSurveyFile).toList(); - - int index = 1; - for (FileAttachment file : files) { - - if(!ossService.isFileExist(file)){ - System.out.println("해당 파일 없음"); - continue; - } + Survey survey = surveyFindService.getSurveyFromId(surveyId); - String zipEntryName = "Q" + question.getQuestionOrder() + "/" + index + "_" + file.getOriginalName(); - zos.putNextEntry(new ZipEntry(zipEntryName)); + // 미리 필요한 질문만 필터 + List questions = survey.getQuestions().stream() + .filter(q -> q.getQuestionType() == QuestionType.FILE || q.getQuestionType() == QuestionType.IMAGE) + .toList(); - try (OSSObject ossObject = ossService.getObject(file); - InputStream inputStream = ossObject.getObjectContent()) { - inputStream.transferTo(zos); + StreamingResponseBody body = out -> { + try (ZipOutputStream zos = new ZipOutputStream(out)) { + for (Question question : questions) { + // ⚠️ N+1이면 여기서 answers 일괄조회/페치조인으로 최적화 권장 + List answers = answerRepository.findAnswerByQuestionRealId(question.getQuestionId()); + + int idx = 1; + for (Answer a : answers) { + FileAttachment file = a.getSurveyFile(); + if (file == null) continue; + + // (선택) 존재 확인 호출이 비싸면 getObject 404로 분기하거나 스킵 + if (!ossService.isFileExist(file)) { + log.debug("file missing: {}", file.getStoredName()); + continue; + } + + String entryName = "Q" + question.getQuestionOrder() + "/" + + idx++ + "_" + file.getOriginalName(); + + zos.putNextEntry(new ZipEntry(entryName)); + try (OSSObject obj = ossService.getObject(file); + InputStream in = obj.getObjectContent()) { + in.transferTo(zos); + } + zos.closeEntry(); } - - zos.closeEntry(); - index++; } + zos.finish(); } - zos.finish(); // 명시적 종료 - } catch (IOException e) { - log.warn(e.getMessage()); - throw new RuntimeException(e); - } + }; - byte[] zipBytes = baos.toByteArray(); - log.info("zip 생성 완료"); + ContentDisposition cd = ContentDisposition.attachment() + .filename(surveyId + ".zip", StandardCharsets.UTF_8) + .build(); return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + surveyId + ".zip\"") + .header(HttpHeaders.CONTENT_DISPOSITION, cd.toString()) .contentType(MediaType.APPLICATION_OCTET_STREAM) - .contentLength(zipBytes.length) - .body(zipBytes); + .body(body); } diff --git a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveySaveService.java b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveySaveService.java index 0ed13f2..e24d857 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveySaveService.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/domain/survey/service/homepage/SurveySaveService.java @@ -89,15 +89,10 @@ public void deleteRemovedQuestions(Survey survey, List questions){ @Transactional public void save(Survey survey, List questions, List options){ - try { surveyRepository.save(survey); questionRepository.saveAll(questions); questionOptionRepository.saveAll(options); log.info("설문조사 저장 완료"); - }catch (Exception e){ - log.warn(e.getMessage()); - log.warn("설문조사 저장 실패"); - } } //HtmlUtils.htmlEscape(question); public List getQuestionsFromDto(List request, Survey survey){ @@ -114,7 +109,6 @@ public List getQuestionsFromDto(List request, Survey surv question.setQuestionType(questionType); question.setOption(QuestionType.checkType(questionType)); question.setQuestionOrder(i++); - question.setTemplate(dto.isTemplate()); question.setDomainField(dto.domainField()); question.setTemplate(dto.isTemplate()); questions.add(question); @@ -195,7 +189,10 @@ public AnswerPageDto getSurveys(String surveyId){ List response = new ArrayList<>(); Survey survey = surveyFindService.getSurveyFromId(surveyId); - if(!survey.isShare()) return null; + boolean expired = (survey.getEndDate() != null) && LocalDate.now().isAfter(survey.getEndDate()); // today > endDate + + if(!survey.isShare()) throw new BaseException(ErrorCode.SURVEY_NOT_SHARE); + if(expired) throw new BaseException(ErrorCode.SURVEY_DATE_NOT_VALID); List questions = surveyFindService.getQuestions(survey); List options = surveyFindService.getOptions(survey); diff --git a/src/main/java/com/cooperation/project/cooperationcenter/global/config/SecurityConfig.java b/src/main/java/com/cooperation/project/cooperationcenter/global/config/SecurityConfig.java index 2c68b86..d3e3f3f 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/global/config/SecurityConfig.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/global/config/SecurityConfig.java @@ -45,8 +45,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ //note static 해제 .requestMatchers("/css/**","/plugins/**","/js/**").permitAll() - //fixme 임시용임 밑에는 + .requestMatchers("/v3/**", + "/swagger-ui.html", + "/swagger-ui/**", + "/swagger-resources/**", + "/api-test/**").permitAll() // .requestMatchers("/api/v1/**").permitAll() // .requestMatchers("/**").permitAll() //note 일반 사용자 페이지 @@ -61,7 +65,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ //note 로그인한 사용자 .requestMatchers("/survey/log/detail/**").authenticated() - //note admin 페이지 .requestMatchers("/admin/login").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") @@ -100,6 +103,7 @@ public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); configuration.addAllowedOriginPattern("http://172.30.1.70:8081"); + configuration.addAllowedOriginPattern("http://localhost:8081"); configuration.addAllowedOriginPattern("https://11680c706486.ngrok-free.app"); configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); diff --git a/src/main/java/com/cooperation/project/cooperationcenter/global/config/SwaggerConfig.java b/src/main/java/com/cooperation/project/cooperationcenter/global/config/SwaggerConfig.java new file mode 100644 index 0000000..d37c26b --- /dev/null +++ b/src/main/java/com/cooperation/project/cooperationcenter/global/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package com.cooperation.project.cooperationcenter.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; + +public class SwaggerConfig { + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("CooperationCenter") // API의 제목 + .description("유학원 홍보 웹사이트") // API에 대한 설명 + .version("1.0.0"); // API의 버전 + } +} diff --git a/src/main/java/com/cooperation/project/cooperationcenter/global/exception/codes/ErrorCode.java b/src/main/java/com/cooperation/project/cooperationcenter/global/exception/codes/ErrorCode.java index e20f298..e51477a 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/global/exception/codes/ErrorCode.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/global/exception/codes/ErrorCode.java @@ -69,11 +69,11 @@ public enum ErrorCode implements BaseCode { //바인딩 에러 BINDING_ERROR(HttpStatus.BAD_REQUEST, "BINDING-0000", "바인딩에 실패했습니다."), - FILE_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "File-0000", "파일 확장자가 잘못되었습니다."), - FILE_READ_ERROR(HttpStatus.BAD_REQUEST, "File-0001", "파일을 읽어오는데 실패하였습니다."), - FILE_NOT_FOUND(HttpStatus.BAD_REQUEST, "File-0002", "해당 ID를 가진 파일을 찾을 수 없습니다."), - FILELOG_NOT_FOUND(HttpStatus.BAD_REQUEST, "File-0003", "파일 업로드 기록을 가져올 수 없습니다."), - FILE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "File-0004", "파일 사이즈가 너무 큽니다."), + FILE_EXTENSION_ERROR(HttpStatus.BAD_REQUEST, "FILE-0000", "파일 확장자가 잘못되었습니다."), + FILE_READ_ERROR(HttpStatus.BAD_REQUEST, "FILE-0001", "파일을 읽어오는데 실패하였습니다."), + FILE_NOT_FOUND(HttpStatus.BAD_REQUEST, "FILE-0002", "해당 ID를 가진 파일을 찾을 수 없습니다."), + FILELOG_NOT_FOUND(HttpStatus.BAD_REQUEST, "FILE-0003", "파일 업로드 기록을 가져올 수 없습니다."), + FILE_SIZE_ERROR(HttpStatus.BAD_REQUEST, "FILE-0004", "파일 사이즈가 너무 큽니다."), //로그인 에러 EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "LOGIN-0000", "이메일이 잘못됨"), @@ -95,6 +95,7 @@ public enum ErrorCode implements BaseCode { MEMBER_NOT_ADMIN(HttpStatus.BAD_REQUEST,"MEMBER-0004","Meber is not ADMIN"), SURVEY_DATE_NOT_VALID(HttpStatus.BAD_REQUEST,"SURVEY-0000","지금 설문조사 입력 기간이 아닙니다."), + SURVEY_NOT_SHARE(HttpStatus.BAD_REQUEST,"SURVEY-0001","해당 설문조사는 공개 전입니다."), // 5xx : server error INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SERVER-0000", "서버 에러"); diff --git a/src/main/java/com/cooperation/project/cooperationcenter/global/filter/AuthenticationTokenFilter.java b/src/main/java/com/cooperation/project/cooperationcenter/global/filter/AuthenticationTokenFilter.java index 21bffa9..e6c1a99 100644 --- a/src/main/java/com/cooperation/project/cooperationcenter/global/filter/AuthenticationTokenFilter.java +++ b/src/main/java/com/cooperation/project/cooperationcenter/global/filter/AuthenticationTokenFilter.java @@ -36,6 +36,16 @@ protected void doFilterInternal(HttpServletRequest request, String path = request.getServletPath(); log.info("진입 path:{}",path); + if ("/v3/api-docs".equals(path) || + path.startsWith("/v3/api-docs/") || + path.equals("/swagger-ui.html") || + path.startsWith("/swagger-ui/") || + path.startsWith("/swagger-resources/") || + path.startsWith("/api-test/")) { + filterChain.doFilter(request, response); + return; + } + //note 무시하는 endpoint들 final String[] IGNORE_PATHS = { "/css", "/js", "/plugins","/member/logout","/member/signup","/api/v1/member","/api/v1/admin","/api/v1/file/img","/admin/login","/favicon.ico","/api/v1/tencent" @@ -74,26 +84,11 @@ protected void doFilterInternal(HttpServletRequest request, } catch (ExpiredJwtException e) { log.warn(e.getMessage()); log.warn("authentication에서 오류발생"); + response.sendRedirect("/member/login"); request.setAttribute("tokenExpired", true); // 포워드 시 전달용 } } SecurityContextHolder.clearContext(); filterChain.doFilter(request, response); } - - - private void sendErrorResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException { - // 1) HTTP 상태 및 인코딩/타입 설정 - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json; charset=UTF-8"); - response.setCharacterEncoding("UTF-8"); - - // 2) BaseResponse 형식으로 에러 객체 생성 - BaseResponse errorBody = BaseResponse.onFailure(errorCode.getCode(), errorCode.getMessage(), null); - - // 3) JSON 직렬화 후 쓰기 - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writeValueAsString(errorBody); - response.getWriter().write(json); - } } diff --git a/src/main/resources/static/js/homepage/login.js b/src/main/resources/static/js/homepage/common/login.js similarity index 97% rename from src/main/resources/static/js/homepage/login.js rename to src/main/resources/static/js/homepage/common/login.js index 7ee3b20..87ff941 100644 --- a/src/main/resources/static/js/homepage/login.js +++ b/src/main/resources/static/js/homepage/common/login.js @@ -1,72 +1,72 @@ -async function loadPageContent() { - const response = await fetch(window.location.pathname, { - credentials: 'include' - }); - - const contentType = response.headers.get('Content-Type'); - - if (contentType && contentType.includes('application/json')) { - const result = await response.json(); - - if (result.code === 'TOKEN-0000') { - const shouldRefresh = confirm("세션이 만료되었습니다. 연장하시겠습니까?"); - if (shouldRefresh) { - const refreshRes = await fetch('/api/v1/member/refresh', { - method: 'POST', - credentials: 'include' - }); - const refreshJson = await refreshRes.json(); - if (refreshJson.isSuccess) { - location.reload(); // 새로고침 (토큰 갱신 후) - } else { - alert("다시 로그인해주세요."); - window.location.href = "/member/login"; - } - } else { - window.location.href = "/member/login"; - } - return; - } - } - - // JSON 아니면 일반 HTML로 간주 → 그대로 렌더링 - const html = await response.text(); - document.open(); - document.write(html); - document.close(); -} - -const webLoginBtn = document.getElementById("web-login-button"); -const mobileLoginBtn = document.getElementById("mobile-login-button"); -if (webLoginBtn) setupLoginLogoutHandler(webLoginBtn); -if (mobileLoginBtn) setupLoginLogoutHandler(mobileLoginBtn); - -function setupLoginLogoutHandler(btn) { - if (!btn) return; - - btn.addEventListener("click", () => { - const label = btn.textContent.trim(); - - if (label === "로그인") { - window.location.href = "/member/login"; - } else if(label === "로그아웃") { - console.log("로그아웃 버튼 클릭"); - fetch("/api/v1/member/logout", { - method: "POST", - credentials: "include" - }).then(response => { - if (response.ok) { - window.location.href = "/home"; - } else { - alert("로그아웃 실패"); - } - }).catch(err => { - console.error("로그아웃 오류:", err); - alert("오류 발생"); - }); - }else { - window.location.href = "/profile"; - - } - }); -} +async function loadPageContent() { + const response = await fetch(window.location.pathname, { + credentials: 'include' + }); + + const contentType = response.headers.get('Content-Type'); + + if (contentType && contentType.includes('application/json')) { + const result = await response.json(); + + if (result.code === 'TOKEN-0000') { + const shouldRefresh = confirm("세션이 만료되었습니다. 연장하시겠습니까?"); + if (shouldRefresh) { + const refreshRes = await fetch('/api/v1/member/refresh', { + method: 'POST', + credentials: 'include' + }); + const refreshJson = await refreshRes.json(); + if (refreshJson.isSuccess) { + location.reload(); // 새로고침 (토큰 갱신 후) + } else { + alert("다시 로그인해주세요."); + window.location.href = "/member/login"; + } + } else { + window.location.href = "/member/login"; + } + return; + } + } + + // JSON 아니면 일반 HTML로 간주 → 그대로 렌더링 + const html = await response.text(); + document.open(); + document.write(html); + document.close(); +} + +const webLoginBtn = document.getElementById("web-login-button"); +const mobileLoginBtn = document.getElementById("mobile-login-button"); +if (webLoginBtn) setupLoginLogoutHandler(webLoginBtn); +if (mobileLoginBtn) setupLoginLogoutHandler(mobileLoginBtn); + +function setupLoginLogoutHandler(btn) { + if (!btn) return; + + btn.addEventListener("click", () => { + const label = btn.textContent.trim(); + + if (label === "로그인") { + window.location.href = "/member/login"; + } else if(label === "로그아웃") { + console.log("로그아웃 버튼 클릭"); + fetch("/api/v1/member/logout", { + method: "POST", + credentials: "include" + }).then(response => { + if (response.ok) { + window.location.href = "/home"; + } else { + alert("로그아웃 실패"); + } + }).catch(err => { + console.error("로그아웃 오류:", err); + alert("오류 발생"); + }); + }else { + window.location.href = "/profile"; + + } + }); +} diff --git a/src/main/resources/static/js/homepage/nav-user-mobile.js b/src/main/resources/static/js/homepage/common/nav-user-mobile.js similarity index 100% rename from src/main/resources/static/js/homepage/nav-user-mobile.js rename to src/main/resources/static/js/homepage/common/nav-user-mobile.js diff --git a/src/main/resources/static/js/homepage/nav-user.js b/src/main/resources/static/js/homepage/common/nav-user.js similarity index 97% rename from src/main/resources/static/js/homepage/nav-user.js rename to src/main/resources/static/js/homepage/common/nav-user.js index 2dd8a13..345652e 100644 --- a/src/main/resources/static/js/homepage/nav-user.js +++ b/src/main/resources/static/js/homepage/common/nav-user.js @@ -1,41 +1,41 @@ -document.addEventListener("DOMContentLoaded", () => { - const mobileBtn = document.getElementById("mobile-menu-button"); - const mobileMenu = document.getElementById("mobile-menu"); - const mobileUniBtn = document.getElementById("mobile-university-button"); - const mobileUniMenu = document.getElementById("mobile-university-menu"); - const uniBtn = document.getElementById("university-button"); - const uniMenu = document.getElementById("university-menu"); - - // 모바일 전체 메뉴 토글 - if (mobileBtn && mobileMenu) { - mobileBtn.addEventListener("click", (e) => { - e.stopPropagation(); - mobileMenu.classList.toggle("hidden"); - console.log(mobileMenu); - }); - } - - // 모바일 대학교 하위 메뉴 토글 - if (mobileUniBtn && mobileUniMenu) { - mobileUniBtn.addEventListener("click", (e) => { - console.log("univ menu btn click"); - e.stopPropagation(); - mobileUniMenu.classList.toggle("hidden"); - mobileUniBtn.querySelector("i").classList.toggle("rotate-180"); - }); - } - - // 데스크탑 대학교 메뉴 토글 - if (uniBtn && uniMenu) { - uniBtn.addEventListener("click", (e) => { - e.stopPropagation(); - uniMenu.classList.toggle("hidden"); - }); - - document.addEventListener("click", (e) => { - if (!uniBtn.contains(e.target) && !uniMenu.contains(e.target)) { - uniMenu.classList.add("hidden"); - } - }); - } +document.addEventListener("DOMContentLoaded", () => { + const mobileBtn = document.getElementById("mobile-menu-button"); + const mobileMenu = document.getElementById("mobile-menu"); + const mobileUniBtn = document.getElementById("mobile-university-button"); + const mobileUniMenu = document.getElementById("mobile-university-menu"); + const uniBtn = document.getElementById("university-button"); + const uniMenu = document.getElementById("university-menu"); + + // 모바일 전체 메뉴 토글 + if (mobileBtn && mobileMenu) { + mobileBtn.addEventListener("click", (e) => { + e.stopPropagation(); + mobileMenu.classList.toggle("hidden"); + console.log(mobileMenu); + }); + } + + // 모바일 대학교 하위 메뉴 토글 + if (mobileUniBtn && mobileUniMenu) { + mobileUniBtn.addEventListener("click", (e) => { + console.log("univ menu btn click"); + e.stopPropagation(); + mobileUniMenu.classList.toggle("hidden"); + mobileUniBtn.querySelector("i").classList.toggle("rotate-180"); + }); + } + + // 데스크탑 대학교 메뉴 토글 + if (uniBtn && uniMenu) { + uniBtn.addEventListener("click", (e) => { + e.stopPropagation(); + uniMenu.classList.toggle("hidden"); + }); + + document.addEventListener("click", (e) => { + if (!uniBtn.contains(e.target) && !uniMenu.contains(e.target)) { + uniMenu.classList.add("hidden"); + } + }); + } }); \ No newline at end of file diff --git a/src/main/resources/static/js/homepage/url.js b/src/main/resources/static/js/homepage/common/url.js similarity index 96% rename from src/main/resources/static/js/homepage/url.js rename to src/main/resources/static/js/homepage/common/url.js index 4962e3a..6891e27 100644 --- a/src/main/resources/static/js/homepage/url.js +++ b/src/main/resources/static/js/homepage/common/url.js @@ -1,13 +1,13 @@ -const surveyUrl = "/api/v1/survey" -const surveyAdminUrl = "/api/v1/survey/admin" - -const memberUrl = "/api/v1/member" -const memberAdminUrl = "/api/v1/member/admin" - -const profileUrl = "/api/v1/profile" - - -const fileUrl="/api/v1/file" -const fileAdminUrl="/api/v1/file" - +const surveyUrl = "/api/v1/survey" +const surveyAdminUrl = "/api/v1/survey/admin" + +const memberUrl = "/api/v1/member" +const memberAdminUrl = "/api/v1/member/admin" + +const profileUrl = "/api/v1/profile" + + +const fileUrl="/api/v1/file" +const fileAdminUrl="/api/v1/file" + // \ No newline at end of file diff --git a/src/main/resources/static/js/homepage/lib/apiClient.js b/src/main/resources/static/js/homepage/lib/apiClient.js new file mode 100644 index 0000000..bf0aba2 --- /dev/null +++ b/src/main/resources/static/js/homepage/lib/apiClient.js @@ -0,0 +1,161 @@ +// /static/js/lib/apiClient.js +import { Notifier } from "./notifier.js"; + +const DEFAULTS = { + baseURL: "", // 필요 시 "/api/v1" + csrfHeader: "X-CSRF-TOKEN", // 사용 중이면 이름 맞춰 넣기 + csrfTokenSelector: 'meta[name="_csrf"]', + // 서버 ErrorCode 매핑(원하는 문구로 쉽게 덮어쓰기 가능) + errorMap: { + "FILE-0000": "허용되지 않는 확장자입니다.", + "FILE-0001": "파일을 읽는 중 오류가 발생했습니다.", + "FILE-0002": "해당 파일을 찾을 수 없습니다.", + "FILE-0003": "파일 업로드 기록을 가져올 수 없습니다.", + "FILE-0004": "파일 사이즈가 너무 큽니다.", + "COMMON400": "잘못된 요청입니다.", + "COMMON401": "인증이 필요합니다.", + "COMMON403": "접근이 금지되었습니다.", + "COMMON500": "서버 에러가 발생했습니다." + } +}; + +export const api = { + config: { ...DEFAULTS }, + + setBaseURL(url) { this.config.baseURL = url; }, + setErrorMap(map) { this.config.errorMap = { ...this.config.errorMap, ...map }; }, + setCsrf(headerName, selector = 'meta[name="_csrf"]') { + this.config.csrfHeader = headerName; this.config.csrfTokenSelector = selector; + }, + + // ---------- public methods ---------- + get(url, opts = {}) { return this._request("GET", url, null, opts); }, + delete(url, opts = {}) { return this._request("DELETE", url, null, opts); }, + postJson(url, json, opts={}) { return this._request("POST", url, JSON.stringify(json), { ...opts, headers: { "Content-Type": "application/json", ...(opts.headers||{}) } }); }, + putJson(url, json, opts={}) { return this._request("PUT", url, JSON.stringify(json), { ...opts, headers: { "Content-Type": "application/json", ...(opts.headers||{}) } }); }, + postMultipart(url, formData, opts={}) { return this._request("POST", url, formData, opts); }, + patchMultipart(url, formData, opts = {}) {return this._request("PATCH", url, formData, opts);}, + patchJson(url, json, opts = {}) { return this._request("PATCH",url,JSON.stringify(json),{ ...opts, headers: { "Content-Type": "application/json", ...(opts.headers || {}) } }); }, + + // ---------- core ---------- + async _request(method, url, body, opts) { + const full = this.config.baseURL ? this.config.baseURL + url : url; + + const headers = new Headers(opts?.headers || {}); + // CSRF 자동 주입(있을 때만) + const tokenEl = document.querySelector(this.config.csrfTokenSelector); + if (tokenEl && !headers.has(this.config.csrfHeader)) { + headers.set(this.config.csrfHeader, tokenEl.getAttribute("content")); + } + + let res; + try { + res = await fetch(full, { + method, + headers, + body, + credentials: opts?.credentials ?? "same-origin", + signal: opts?.signal + }); + } catch (e) { + console.log("res 중에 오류"); + Notifier.modal("네트워크 오류", "서버에 연결할 수 없습니다. 잠시 후 다시 시도해주세요."); + throw e; + } + + console.debug("[api] <-", res.status, res.statusText, "for", method, url); + + if (!res.ok) { + console.log("!res.ok 진입"); + const err = await parseErrorResponse(res); + handleError(err, res.status, this.config.errorMap); + const serverMsg = typeof err.raw === "string" ? err.raw : (err.message || ""); + const e = new Error(serverMsg || `HTTP ${res.status}`); + e.code = err.code || null; + e.status = res.status; + e.payload = err.raw; + throw e; + } + + // content-type 따라 자동 파싱 + const ct = res.headers.get("Content-Type") || ""; + if (ct.includes("application/json")){ + const j = await res.json(); + if (j && typeof j === "object" && "isSuccess" in j) { + if (j.isSuccess === false) { + const err = { + code: j.code ?? null, + message: j.message ?? null, + httpStatus: j.httpStatus ?? 400, // 표시용 상태(서버가 200으로 줘도 OK) + raw: j + }; + console.error("[API ERROR LOGICAL]", err.httpStatus, err.code, err.message, err.raw); + handleError(err, err.httpStatus, this.config.errorMap); + + const e = new Error(err.message || "Logical failure"); + e.code = err.code; e.status = err.httpStatus; e.payload = err.raw; + throw e; + } + return ("result" in j ? j.result : j); + } + return res.json(); + } + if (ct.startsWith("text/")) return res.text(); + return res; // 파일/바이너리 응답 등 + } +}; + +// ---------- helpers ---------- +async function parseErrorResponse(res) { + try { + const r2 = res.clone(); + const ct = r2.headers.get("Content-Type") || ""; + if (ct.includes("application/json")) { + const j = await r2.json(); + + // 다양한 래핑 가능성 방어: {reason:{...}}, {error:{...}}, 최상위 ... + const pick = (obj) => { + if (!obj || typeof obj !== "object") return {}; + if ("code" in obj || "message" in obj || "httpStatus" in obj || "isSuccess" in obj) return obj; + // 흔한 래핑 키 + if (obj.reason) return obj.reason; + if (obj.error) return obj.error; + if (obj.data && (obj.data.reason || obj.data.error)) return obj.data.reason || obj.data.error; + return obj; + }; + + const p = pick(j); + return { + code: p.code ?? null, + message: p.message ?? null, + httpStatus: p.httpStatus ?? res.status, + isSuccess: typeof p.isSuccess === "boolean" ? p.isSuccess : false, + raw: j + }; + } + const t = await r2.text(); + return { code: null, message: t || null, httpStatus: res.status, isSuccess: false, raw: t }; + } catch { + return { code: null, message: null, httpStatus: res.status, isSuccess: false, raw: null }; + } +} + +function handleError(err, status, errorMap) { + // HTTP 상태 우선 처리 + if (status === 413) return Notifier.modal("업로드 실패", "파일이 너무 큽니다. 용량을 줄여 다시 시도해주세요. (413)"); + if (status === 415) return Notifier.modal("요청 형식 오류", "지원하지 않는 콘텐츠 형식입니다. (415)"); + if (status === 403) return Notifier.modal("권한 없음", "요청이 거부되었습니다. 로그인/권한을 확인해주세요. (403)"); + if (status === 401) return Notifier.modal("인증 필요", "로그인 세션이 만료되었을 수 있습니다. 다시 로그인해주세요. (401)"); + + // 서버 커스텀 코드/메시지 + if (err.code && errorMap[err.code]) { + return Notifier.modal("요청 실패", errorMap[err.code]); + } + if (err.message) { + return Notifier.modal("요청 실패", err.message); + } + Notifier.modal("요청 실패", "알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); +} + +// (선택) 레거시 페이지 지원 +// window.api = api; diff --git a/src/main/resources/static/js/homepage/lib/notifier.js b/src/main/resources/static/js/homepage/lib/notifier.js new file mode 100644 index 0000000..f3951e9 --- /dev/null +++ b/src/main/resources/static/js/homepage/lib/notifier.js @@ -0,0 +1,57 @@ +// /static/js/lib/notifier.js +export const Notifier = { + modal(title, message, okText = "닫기") { + const wrap = document.createElement("div"); + wrap.className = "fixed inset-0 z-[9999] flex items-center justify-center"; + wrap.style.background = "rgba(0,0,0,.45)"; + + const box = document.createElement("div"); + box.style.maxWidth = "520px"; + box.style.width = "92%"; + box.style.background = "#fff"; + box.style.borderRadius = "16px"; + box.style.padding = "20px 24px"; + box.style.boxShadow = "0 10px 30px rgba(0,0,0,.2)"; + box.innerHTML = ` +
+
!
+
+

${escapeHtml(title)}

+

${escapeHtml(message)}

+
+ +
+ `; + wrap.appendChild(box); + wrap.addEventListener("click", (e) => { if (e.target === wrap) wrap.remove(); }); + box.querySelector("#__notifier_ok__").onclick = () => wrap.remove(); + document.body.appendChild(wrap); + }, + + toast(message, ms = 2000) { + const el = document.createElement("div"); + el.textContent = message; + el.style.position = "fixed"; + el.style.left = "50%"; + el.style.bottom = "22px"; + el.style.transform = "translateX(-50%)"; + el.style.background = "#111827"; + el.style.color = "#fff"; + el.style.padding = "10px 14px"; + el.style.borderRadius = "10px"; + el.style.zIndex = "9999"; + el.style.boxShadow = "0 6px 20px rgba(0,0,0,.25)"; + document.body.appendChild(el); + setTimeout(() => el.remove(), ms); + } +}; + +function escapeHtml(s) { + if (typeof s !== "string") return ""; + return s.replace(/[&<>"'`=\/]/g, c => ({ + "&":"&","<":"<",">":">","\"":""","'":"'","`":"`","=":"=","/":"/" + }[c])); +} + +// (선택) 모듈 사용이 어려운 레거시 페이지 지원 +// window.Notifier = Notifier; \ No newline at end of file diff --git a/src/main/resources/templates/adminpage/common/leftSide.html b/src/main/resources/templates/adminpage/common/leftSide.html index 9b1132f..d84e2be 100644 --- a/src/main/resources/templates/adminpage/common/leftSide.html +++ b/src/main/resources/templates/adminpage/common/leftSide.html @@ -66,17 +66,17 @@

logo

학생 관리 -
  • - -
    - -
    - 시스템 설정 -
    -
  • + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/adminpage/user/index.html b/src/main/resources/templates/adminpage/user/index.html index d7a30eb..54c0fcd 100644 --- a/src/main/resources/templates/adminpage/user/index.html +++ b/src/main/resources/templates/adminpage/user/index.html @@ -71,14 +71,9 @@

    관리자 대시보드

    시스템 현황을 한눈에 확인하세요

    -
    -
    - -
    - 5개 대기 -
    + + +

    김관리자

    최고 관리자

    @@ -150,7 +145,7 @@

    >

    - 최근 로그인 기록 - 수정 필요 + 최근 로그인 기록
    @@ -557,7 +481,6 @@

    회원가입 승인

    modal.classList.remove("hidden"); modal.classList.add("flex"); } - function hideModal(modal) { modal.classList.add("hidden"); modal.classList.remove("flex"); @@ -565,63 +488,45 @@

    회원가입 승인

    const viewDetailsButtons = document.querySelectorAll(".view-details-btn"); - viewDetailsButtons.forEach((button) => { button.addEventListener("click", function () { const row = this.closest("tr"); - const name = row.cells[1].textContent; - const email = row.cells[2].textContent; - const joinDate = row.cells[3].textContent; - const lastLogin = "2025-07-11" + const name = row.cells[0]?.textContent?.trim() || "-"; + const email = row.cells[1]?.textContent?.trim() || "-"; + const joinDate = row.cells[2]?.textContent?.trim() || "-"; + const lastLogin = "2025-07-11"; const isActive = true; + document.getElementById("detail-name").textContent = name; document.getElementById("detail-email").textContent = email; document.getElementById("detail-join-date").textContent = joinDate; document.getElementById("detail-last-login").textContent = lastLogin; - document.getElementById("detail-status").textContent = isActive - ? "활성" - : "비활성"; - document.getElementById( - "detail-status", - ).previousElementSibling.className = isActive - ? "w-2 h-2 rounded-full bg-green-500 mr-2" - : "w-2 h-2 rounded-full bg-red-500 mr-2"; + document.getElementById("detail-status").textContent = isActive ? "활성" : "비활성"; + document.getElementById("detail-status").previousElementSibling.className = + isActive ? "w-2 h-2 rounded-full bg-green-500 mr-2" : "w-2 h-2 rounded-full bg-red-500 mr-2"; + showModal(userDetailsModal); }); }); - document - .getElementById("close-details") - .addEventListener("click", () => hideModal(userDetailsModal)); - document - .getElementById("close-details-btn") - .addEventListener("click", () => hideModal(userDetailsModal)); - const documentPreviewModal = document.getElementById( - "document-preview-modal", - ); - const viewBusinessRegistrationBtn = document.getElementById( - "view-business-registration", - ); + + document.getElementById("close-details").addEventListener("click", () => hideModal(userDetailsModal)); + document.getElementById("close-details-btn").addEventListener("click", () => hideModal(userDetailsModal)); + + const documentPreviewModal = document.getElementById("document-preview-modal"); + const viewBusinessRegistrationBtn = document.getElementById("view-business-registration"); const closePreviewButtons = document.querySelectorAll(".close-preview"); - viewBusinessRegistrationBtn.addEventListener("click", () => { - showModal(documentPreviewModal); - }); + + viewBusinessRegistrationBtn.addEventListener("click", () => showModal(documentPreviewModal)); closePreviewButtons.forEach((button) => { button.addEventListener("click", () => hideModal(documentPreviewModal)); }); - [ - userDetailsModal, - documentPreviewModal, - ].forEach((modal) => { + + [userDetailsModal, documentPreviewModal].forEach((modal) => { modal.addEventListener("click", function (e) { - if (e.target === modal) { - hideModal(modal); - } + if (e.target === modal) hideModal(modal); }); }); }); - - - + + + + + diff --git a/src/main/resources/templates/adminpage/user/login.html b/src/main/resources/templates/adminpage/user/login.html index 330d8aa..4099383 100644 --- a/src/main/resources/templates/adminpage/user/login.html +++ b/src/main/resources/templates/adminpage/user/login.html @@ -333,6 +333,6 @@

    로그인 실패

    }); }); - + diff --git a/src/main/resources/templates/adminpage/user/member/manageUser.html b/src/main/resources/templates/adminpage/user/member/manageUser.html index c54dab5..8f0ae1b 100644 --- a/src/main/resources/templates/adminpage/user/member/manageUser.html +++ b/src/main/resources/templates/adminpage/user/member/manageUser.html @@ -157,14 +157,14 @@

    사용자 관리

    -
    -
    - -
    - 5개 대기 -
    + + + + + + + +

    김관리자

    최고 관리자

    diff --git a/src/main/resources/templates/adminpage/user/school/manageSchool.html b/src/main/resources/templates/adminpage/user/school/manageSchool.html index 3b5a48d..22bf274 100644 --- a/src/main/resources/templates/adminpage/user/school/manageSchool.html +++ b/src/main/resources/templates/adminpage/user/school/manageSchool.html @@ -1191,8 +1191,8 @@

    일정 목록

    @@ -1320,8 +1320,8 @@

    일정 목록

    diff --git a/src/main/resources/templates/adminpage/user/student/studentList.html b/src/main/resources/templates/adminpage/user/student/studentList.html index 5194a6e..0a9f630 100644 --- a/src/main/resources/templates/adminpage/user/student/studentList.html +++ b/src/main/resources/templates/adminpage/user/student/studentList.html @@ -272,54 +272,30 @@

    학생 목록

    ID -
    - -
    중국어 이름 -
    - -
    영어 이름 -
    - -
    생년월일 -
    - -
    성별 @@ -769,61 +745,6 @@

    설문 선택

    return { name, gender, birthStart, birthEnd, email, passport, exam, surveyLogId, surveyTitle,agencyName }; } - window.sortTable = function (field) { - if (currentSort.field === field) { - currentSort.direction = currentSort.direction === "asc" ? "desc" : "asc"; - } else { - currentSort.field = field; - currentSort.direction = "asc"; - } - const tbody = document.getElementById("student-table-body"); - const rows = Array.from(tbody.getElementsByTagName("tr")); - rows.sort((a, b) => { - let aValue = a.cells[getColumnIndex(field)].textContent.trim(); - let bValue = b.cells[getColumnIndex(field)].textContent.trim(); - if (field === "id") { - aValue = parseInt(aValue.replace("#", "")); - bValue = parseInt(bValue.replace("#", "")); - } - if (field === "birthday") { - aValue = new Date(aValue); - bValue = new Date(bValue); - } - if (currentSort.direction === "asc") { - return aValue > bValue ? 1 : -1; - } else { - return aValue < bValue ? 1 : -1; - } - }); - rows.forEach((row) => tbody.appendChild(row)); - updateSortingIcons(field); - showNotification( - `${field} 기준으로 ${currentSort.direction === "asc" ? "오름차순" : "내림차순"} 정렬되었습니다`, - ); - }; - function getColumnIndex(field) { - const fieldMap = { - id: 0, - chineseName: 1, - englishName: 2, - birthday: 3, - }; - return fieldMap[field] || 0; - } - function updateSortingIcons(currentField) { - const headers = document.querySelectorAll("th[onclick]"); - headers.forEach((header) => { - const icon = header.querySelector("i"); - if (header.onclick.toString().includes(currentField)) { - icon.className = - currentSort.direction === "asc" - ? "ri-arrow-up-line text-primary text-xs" - : "ri-arrow-down-line text-primary text-xs"; - } else { - icon.className = "ri-arrow-up-down-line text-xs"; - } - }); - } window.closeStudentModal = function () { document.getElementById("studentDetailModal").classList.add("hidden"); }; diff --git a/src/main/resources/templates/homepage/common/nav-user.html b/src/main/resources/templates/homepage/common/nav-user.html index d10661a..fd7cbc1 100644 --- a/src/main/resources/templates/homepage/common/nav-user.html +++ b/src/main/resources/templates/homepage/common/nav-user.html @@ -103,8 +103,7 @@ - - - - + + + diff --git a/src/main/resources/templates/homepage/common/survey-make-nav.html b/src/main/resources/templates/homepage/common/survey-make-nav.html new file mode 100644 index 0000000..40235ec --- /dev/null +++ b/src/main/resources/templates/homepage/common/survey-make-nav.html @@ -0,0 +1,14 @@ +
    + +
    \ No newline at end of file diff --git a/src/main/resources/templates/homepage/common/survey-nav.html b/src/main/resources/templates/homepage/common/survey-nav.html index db4e23b..ee1bf30 100644 --- a/src/main/resources/templates/homepage/common/survey-nav.html +++ b/src/main/resources/templates/homepage/common/survey-nav.html @@ -18,14 +18,14 @@ 설문조사 목록 - + + + + + + + +
    diff --git a/src/main/resources/templates/homepage/user/agency/agency-introduction.html b/src/main/resources/templates/homepage/user/agency/agency-introduction.html index 1719858..1e6b64b 100644 --- a/src/main/resources/templates/homepage/user/agency/agency-introduction.html +++ b/src/main/resources/templates/homepage/user/agency/agency-introduction.html @@ -591,5 +591,9 @@

    연락처

    }); }); + diff --git a/src/main/resources/templates/homepage/user/forgetPassword.html b/src/main/resources/templates/homepage/user/forgetPassword.html index f15af3d..a4c7ee1 100644 --- a/src/main/resources/templates/homepage/user/forgetPassword.html +++ b/src/main/resources/templates/homepage/user/forgetPassword.html @@ -121,7 +121,14 @@

    비밀번호 찾기

    - + + + + + + - + diff --git a/src/main/resources/templates/homepage/user/member/profile.html b/src/main/resources/templates/homepage/user/member/profile.html index fd38e26..e1ee78e 100644 --- a/src/main/resources/templates/homepage/user/member/profile.html +++ b/src/main/resources/templates/homepage/user/member/profile.html @@ -745,6 +745,14 @@

    logo

    + + + - - diff --git a/src/main/resources/templates/homepage/user/resetPassword.html b/src/main/resources/templates/homepage/user/resetPassword.html index d9bbac9..402793c 100644 --- a/src/main/resources/templates/homepage/user/resetPassword.html +++ b/src/main/resources/templates/homepage/user/resetPassword.html @@ -121,8 +121,13 @@

    비밀번호 재설정

    + - + - - + + diff --git a/src/main/resources/templates/homepage/user/signup.html b/src/main/resources/templates/homepage/user/signup.html index c45858e..c995b83 100644 --- a/src/main/resources/templates/homepage/user/signup.html +++ b/src/main/resources/templates/homepage/user/signup.html @@ -108,7 +108,7 @@

    유학원 회원가입

    - 대표자 정보 + 사용자 정보

    @@ -116,7 +116,7 @@

    유학원 회원가입

    사용자 이름 * 유학원 회원가입 사용자 전화번호 * 유학원 회원가입 사용자 휴대폰 번호 * 유학원 회원가입 사용자 주소 *
    주소 검색
    + - + diff --git a/src/main/resources/templates/homepage/user/survey/copy-survey-list-admin.html b/src/main/resources/templates/homepage/user/survey/copy-survey-list-admin.html index fc71869..922893c 100644 --- a/src/main/resources/templates/homepage/user/survey/copy-survey-list-admin.html +++ b/src/main/resources/templates/homepage/user/survey/copy-survey-list-admin.html @@ -383,6 +383,13 @@

    설문조사 목록

    + + + + +
    -
    +
    -
    -
    2025년 7월 1일 화요일
    +
    +
    2025년 7월 1일 화요일
    @@ -111,25 +101,20 @@
    +
    +
    -

    고객 만족도 조사 2025

    +

    고객 만족도 조사 2025

    2025.06.20 - 2025.07.01

    - - +
    @@ -154,18 +139,66 @@

    + +
    -

    응답 내용

    + +
    -

    - -
    -
    -

    답변

    + +
    +

    + 질문 내용 +

    +
    + + SHORT + + + ESSAY + + + 객관식 + + + 체크박스 + + + 드롭다운 + + + 날짜 + + + 파일 + + + 이미지 + + + 계층형 + +
    +
    + + +
    +
    +

    답변

    @@ -181,103 +214,117 @@

    +
    미응답

    -
    -
    -

    선택된 항목

    +
    +
    +
    선택
    +
    선택된 항목
    +
    -
    -

    날짜

    +
    + + 날짜
    - +
    -
    +
    +
    + +
    파일명.ext
    +
    +
    + +
    +
    -
    +
    +
    미응답
    +
    - -
    - - - - > - - + +
    + + +
    미응답
    + + + +
    -
    +
    - - - - - - - - - - - - - -
    - + + - \ No newline at end of file + diff --git a/src/main/resources/templates/homepage/user/survey/survey-answer-log.html b/src/main/resources/templates/homepage/user/survey/survey-answer-log.html index ec0a041..81b4295 100644 --- a/src/main/resources/templates/homepage/user/survey/survey-answer-log.html +++ b/src/main/resources/templates/homepage/user/survey/survey-answer-log.html @@ -1094,7 +1094,14 @@
    - + + + + - + + + + + + + + + +