Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d72466f
test: 대휴칼 고전 졸업인증 페이지 정상 파싱 후 결과 정상 반환 여부 판정 테스트
haeyoon1 Mar 9, 2026
e173ea6
test: 불필요한 출력문 삭제
haeyoon1 Mar 9, 2026
346f20c
feat: 영역 별 이수 권수 기준으로 고전 독서 인증 여부 수정 로직 추가
haeyoon1 Mar 19, 2026
8bab07a
refactor: resolver 클래스 분리
haeyoon1 Mar 19, 2026
e789a64
feat: 영역 별 이수 권수 만족 여부 확인 메서드 작성
haeyoon1 Mar 19, 2026
b9d2be6
test: 고전 독서 보정 로직 테스트
haeyoon1 Mar 19, 2026
d7f3cf9
test: displayname 추가
haeyoon1 Mar 19, 2026
22fdda1
fix: TOSC 로그인 실패 시 전체 로그인 차단 문제 해결
2Jin1031 Mar 21, 2026
722444e
feat: 공지사항 엔티티 생성
boyekim Mar 28, 2026
9b482ab
feat: 공지사항 전체 조회 api 구현
boyekim Mar 28, 2026
4b79bca
feat: 공지사항 등록 api 구현
boyekim Mar 29, 2026
c440c35
refactor: 전체 조회 조건 추가
boyekim Mar 29, 2026
27ae010
refactor: 공지 생성 시 api 호출 레벨에서 검증 추가
boyekim Mar 29, 2026
979eea8
feat: 공지 수정 api 구현
boyekim Mar 29, 2026
a341e4c
feat: 공지 삭제 api 구현
boyekim Mar 29, 2026
ed71ea0
test: 공지 CRUD 단위 테스트 및 객체 협력 테스트 추가
boyekim Mar 29, 2026
fd83c52
feat: MethodArgumentNotValidException 핸들러 추가
boyekim Mar 30, 2026
cee4753
feat: OperationType 전체 카테고리 추가
boyekim Mar 30, 2026
9451ce0
refactor: 개행 추가
2Jin1031 Apr 3, 2026
60bb57b
feat: OperationType 리뷰 카테고리 추가
boyekim Apr 4, 2026
1f11810
feat: 고전 크롤링 실패 시 fallback 적용
haeyoon1 Apr 5, 2026
243c1bf
Merge pull request #308 from allcll/2Jin1031/TSK-56-150
2Jin1031 Apr 5, 2026
6c5affa
refactor: BaseEntity에 `@Getter` 적용
boyekim Apr 5, 2026
b445714
refactor: UserReview에 BaseEntity extends 추가
boyekim Apr 5, 2026
0b78d1a
refactor: Notice에 OperationType null 비허용
boyekim Apr 5, 2026
86348d7
feat: 중복 null 검증 제거 및 로직 위치 수정
haeyoon1 Apr 5, 2026
96d52ef
refactor: Test에 불필요한 entityManager 삭제
boyekim Apr 5, 2026
eb1dd87
feat: BaseEntity에 updatedAt 필드 추가
boyekim Apr 5, 2026
9d9f639
refactor: UpdatedNoticeResponse 필드 수정
boyekim Apr 5, 2026
66f736c
refactor: updatedAt 중간 반영 되도록 로직 추가
boyekim Apr 5, 2026
6cf169b
test: updatedAt 검증 가능하도록 테스트 수정
boyekim Apr 5, 2026
8e4b713
test: updatedAt 수정
boyekim Apr 5, 2026
55ec91b
refactor: 엔티티 생성 책임 DTO로 이관하여 캡슐화
boyekim Apr 7, 2026
a316c0c
test: 공지 수정 테스트 수정 및 BaseEntityListenerTest 추가
boyekim Apr 7, 2026
de12f25
feat: 에러 타입 수정
haeyoon1 Apr 7, 2026
7f4f279
feat: 클라이언트용 공지 조회 api 구현
boyekim Apr 7, 2026
c3684b0
test: 테스트 추가
boyekim Apr 7, 2026
fb5f100
Merge pull request #309 from allcll/boyekim/TSK-69
boyekim Apr 7, 2026
a364f9d
feat: GraduationCheckCertResult.empty() 객체 생성
haeyoon1 Apr 7, 2026
a9f47c9
test: empty 객체 생성으로 인한 테스트코드 수정
haeyoon1 Apr 7, 2026
cf2c485
test: 테스트 오류 수정 및 조건문 위치 수정
haeyoon1 Apr 7, 2026
fb1ed29
Merge pull request #305 from allcll/TSK-56-151
haeyoon1 Apr 7, 2026
1af10b3
feat: loginTosc가 실패 시 예외를 던지도록 변경
2Jin1031 Apr 10, 2026
09f9ed1
Merge pull request #313 from allcll/2Jin1031/TSK-56-150
2Jin1031 Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-mail'
// implementation 'io.micrometer:micrometer-registry-datadog'
Expand Down
74 changes: 74 additions & 0 deletions src/main/java/kr/allcll/backend/admin/notice/AdminNoticeApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package kr.allcll.backend.admin.notice;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import kr.allcll.backend.admin.AdminRequestValidator;
import kr.allcll.backend.admin.notice.dto.CreateNoticeRequest;
import kr.allcll.backend.admin.notice.dto.CreateNoticeResponse;
import kr.allcll.backend.admin.notice.dto.AdminNoticesResponse;
import kr.allcll.backend.admin.notice.dto.UpdateNoticeRequest;
import kr.allcll.backend.admin.notice.dto.UpdateNoticeResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RestController;

@RestController
@RequiredArgsConstructor
public class AdminNoticeApi {

private final AdminNoticeService adminNoticeService;
private final AdminRequestValidator validator;

@GetMapping("/api/admin/notices")
public ResponseEntity<AdminNoticesResponse> getAllNotice(HttpServletRequest request) {
if (validator.isRateLimited(request) || validator.isUnauthorized(request)) {
return ResponseEntity.status(401).build();
}
AdminNoticesResponse response = adminNoticeService.getAllNotice();
return ResponseEntity.ok(response);
}

@PostMapping("/api/admin/notices")
public ResponseEntity<CreateNoticeResponse> createNotice(
HttpServletRequest request,
@Valid @RequestBody CreateNoticeRequest createNoticeRequest
) {
if (validator.isRateLimited(request) || validator.isUnauthorized(request)) {
return ResponseEntity.status(401).build();
}
CreateNoticeResponse response = adminNoticeService.createNewNotice(createNoticeRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PatchMapping("/api/admin/notices/{id}")
public ResponseEntity<UpdateNoticeResponse> modifyNotice(
HttpServletRequest request,
@PathVariable Long id,
@Valid @RequestBody UpdateNoticeRequest updateNoticeRequest
) {
if (validator.isRateLimited(request) || validator.isUnauthorized(request)) {
return ResponseEntity.status(401).build();
}
UpdateNoticeResponse response = adminNoticeService.updateNotice(id, updateNoticeRequest);
return ResponseEntity.ok(response);
}

@DeleteMapping("/api/admin/notices/{id}")
public ResponseEntity<Void> deleteNotice(
HttpServletRequest request,
@PathVariable Long id
) {
if (validator.isRateLimited(request) || validator.isUnauthorized(request)) {
return ResponseEntity.status(401).build();
}
adminNoticeService.deleteNotice(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package kr.allcll.backend.admin.notice;

import java.util.List;
import kr.allcll.backend.admin.notice.dto.CreateNoticeRequest;
import kr.allcll.backend.admin.notice.dto.CreateNoticeResponse;
import kr.allcll.backend.admin.notice.dto.AdminNoticesResponse;
import kr.allcll.backend.admin.notice.dto.UpdateNoticeRequest;
import kr.allcll.backend.admin.notice.dto.UpdateNoticeResponse;
import kr.allcll.backend.domain.notice.Notice;
import kr.allcll.backend.domain.notice.NoticeRepository;
import kr.allcll.backend.support.exception.AllcllErrorCode;
import kr.allcll.backend.support.exception.AllcllException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final NoticeRepository noticeRepository;

public AdminNoticesResponse getAllNotice() {
List<Notice> allNotices = noticeRepository.findAllOrderedByCreatedAt();
return AdminNoticesResponse.from(allNotices);
}

@Transactional
public CreateNoticeResponse createNewNotice(CreateNoticeRequest createNoticeRequest) {
Notice notice = noticeRepository.save(createNoticeRequest.toEntity());
return CreateNoticeResponse.from(notice);
}

@Transactional
public UpdateNoticeResponse updateNotice(Long id, UpdateNoticeRequest updateNoticeRequest) {
Notice notice = noticeRepository.findActiveById(id)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.NOTICE_NOT_FOUND, id));
notice.update(
updateNoticeRequest.title(),
updateNoticeRequest.content(),
updateNoticeRequest.operationType()
);
noticeRepository.flush();
return UpdateNoticeResponse.from(notice);
}

@Transactional
public void deleteNotice(Long id) {
Notice notice = noticeRepository.findActiveById(id)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.NOTICE_NOT_FOUND, id));
softDeleteNotice(notice);
}

private void softDeleteNotice(Notice notice) {
notice.delete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.allcll.backend.admin.notice.dto;

import java.time.LocalDateTime;
import kr.allcll.backend.domain.notice.Notice;
import kr.allcll.backend.domain.operationPeriod.OperationType;

public record AdminNoticeResponse(
long id,
String title,
String content,
OperationType operationType,
LocalDateTime createdAt
) {

public static AdminNoticeResponse from(Notice notice) {
return new AdminNoticeResponse(
notice.getId(),
notice.getTitle(),
notice.getContent(),
notice.getOperationType(),
notice.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.allcll.backend.admin.notice.dto;

import java.util.List;
import kr.allcll.backend.domain.notice.Notice;

public record AdminNoticesResponse(
List<AdminNoticeResponse> notices
) {

public static AdminNoticesResponse from(List<Notice> allNotices) {
return new AdminNoticesResponse(
allNotices.stream()
.map(AdminNoticeResponse::from)
.toList()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.allcll.backend.admin.notice.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import kr.allcll.backend.domain.notice.Notice;
import kr.allcll.backend.domain.operationPeriod.OperationType;

public record CreateNoticeRequest(
@NotBlank
@Size(max = 250)
String title,

@NotBlank
@Size(max = 1000)
String content,

@NotNull
OperationType operationType
) {

public Notice toEntity() {
return Notice.of(
title,
content,
operationType
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.allcll.backend.admin.notice.dto;

import java.time.LocalDateTime;
import kr.allcll.backend.domain.notice.Notice;
import kr.allcll.backend.domain.operationPeriod.OperationType;

public record CreateNoticeResponse(
long id,
String title,
String content,
OperationType operationType,
LocalDateTime createdAt
) {

public static CreateNoticeResponse from(Notice notice) {
return new CreateNoticeResponse(
notice.getId(),
notice.getTitle(),
notice.getContent(),
notice.getOperationType(),
notice.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kr.allcll.backend.admin.notice.dto;

import jakarta.validation.constraints.Size;
import kr.allcll.backend.domain.operationPeriod.OperationType;

public record UpdateNoticeRequest(
@Size(max = 250)
String title,

@Size(max = 1000)
String content,

OperationType operationType
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kr.allcll.backend.admin.notice.dto;

import java.time.LocalDateTime;
import kr.allcll.backend.domain.notice.Notice;
import kr.allcll.backend.domain.operationPeriod.OperationType;

public record UpdateNoticeResponse(
long id,
String title,
String content,
OperationType operationType,
LocalDateTime updatedAt
) {

public static UpdateNoticeResponse from(Notice notice) {
return new UpdateNoticeResponse(
notice.getId(),
notice.getTitle(),
notice.getContent(),
notice.getOperationType(),
notice.getUpdatedAt()
);
}
}
Loading
Loading