diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml
index 308c2112..44d549ad 100644
--- a/.github/workflows/cd-dev.yml
+++ b/.github/workflows/cd-dev.yml
@@ -26,9 +26,10 @@ jobs:
- name: Create application.yml
run: |
pwd
- touch src/main/resources/application-dev.yml
- echo "${{ secrets.APPLICATION_DEV }}" >> src/main/resources/application-dev.yml
- cat src/main/resources/application-dev.yml
+ cd ./operation-api/src/main/resources
+ touch ./application-dev.yml
+ echo "${{ secrets.APPLICATION_DEV }}" >> ./application-dev.yml
+ cat ./application-dev.yml
- name: Build with Gradle
run: ./gradlew build
diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml
index fcf8f2db..19038e1c 100644
--- a/.github/workflows/cd-prod.yml
+++ b/.github/workflows/cd-prod.yml
@@ -26,9 +26,10 @@ jobs:
- name: Create application.yml
run: |
pwd
- touch src/main/resources/application-prod.yml
- echo "${{ secrets.APPLICATION_PROD }}" >> src/main/resources/application-prod.yml
- cat src/main/resources/application-prod.yml
+ cd ./operation-api/src/main/resources
+ touch ./application-prod.yml
+ echo "${{ secrets.APPLICATION_PROD }}" >> ./application-prod.yml
+ cat ./application-prod.yml
- name: Build with Gradle
run: ./gradlew build
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3120e8b6..32b14339 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,9 +26,10 @@ jobs:
- name: Create application.yml
run: |
pwd
- touch src/main/resources/application-dev.yml
- echo "${{ secrets.APPLICATION_DEV }}" >> src/main/resources/application-dev.yml
- cat src/main/resources/application-dev.yml
+ cd ./operation-api/src/main/resources
+ touch ./application-dev.yml
+ echo "${{ secrets.APPLICATION_DEV }}" >> ./application-dev.yml
+ cat ./application-dev.yml
- name: Build with Gradle
run: ./gradlew build
diff --git a/.gitignore b/.gitignore
index 601e1684..5df0f4ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,6 @@ out/
.vscode/
### config yml ###
-application-**.yml
\ No newline at end of file
+application-**.yml
+
+*/src/main/generated
\ No newline at end of file
diff --git a/README.md b/README.md
index ebe176e8..f5199570 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,59 @@
-# sopt-operation-backend
-**메이커스 운영팀 서버** : 출석 관리 어드민 서비스, 회원 출석 체크 서비스, 알림 전송 서비스
+# SOPT 메이커스 운영 프로덕트 서버
+> SOPT 활동 기수 회원과 임원진의 편리한 운영을 위한 서비스를 만들어요.
+
+### 웹 어드민 (임원진 대상)
+- 세미나, 행사 등 **세션 생성**
+- 활동 기수 회원의 **출석 내역 관리**
+- 푸시 알림을 전송할 **공지 및 소식 작성, 푸시알림 전송**
+
+### 출석 앱 (활동 기수 회원 대상)
+- 참여한 세션 **출석 체크**, **자신의 출석 내역 조회**
+
+
+
+## Server Acrchitecture
+
+
+
+
+
+## Used Stacks
+
-## 🛠 Used Stacks
+## 프로젝트 폴더 구조
+### 멀티모듈 구조
+```
+📁 operation-api # Controller, Service
+📁 operation-auth # Authentication 관련 기능
+📁 operation-common # 공통 기능
+📁 operation-domain # Entity
+📁 operation-external # 외부 API 기능(SOPT 메이커스 내 플레이그라운드, 알림TF)
+```
-- Java 17
-- Gradle
-- Spring Boot 2.7.4
-- Spring Data JPA
-- PostgreSQL
+### 모듈 내 구조
+```
+📁 src
+|_ 📁 main
+|_ |_ 📁 app # 앱 기능
+|_ |_ 📁 common # 공통 기능
+|_ |_ 📁 web # 웹 기능
+```
-## 👥 팀원
+## Member
| [이용택](https://github.com/dragontaek-lee)| [김소현](https://github.com/thguss)|
|:-----:|:------:|
| | |
-|- 프로젝트 초기 세팅
- HTTPS 설정
- 회원 출석 체크 서비스|- 프로젝트 초기 세팅
- CICD 환경 구축
- 출석 관리 어드민 서비스|
+|- 프로젝트 초기 세팅
- HTTPS 설정
- (App)회원 출석 체크 기능
- 알림TF|- 프로젝트 초기 세팅
- CICD 환경 구축
- (Web)세션 출석 관리 기능
- 알림 관리 기능|
-## 📏 Process
+## Process
1. 개발 전에 `github issue`를 생성해주세요!
1. 템플릿에 맞게 내용을 작성한다
2. Assignees, Label을 단다
@@ -38,12 +68,10 @@
7. `approve`가 완료되었다면 `merge`를 진행해주세!요
> `코드 외적인 부분`(환경변수, db 필드 및 테이블 수정, 인프라 세팅 등) 수정사항이 있다면 팀원에게 먼저 물어보고 진행하거나, 그러지 못하였더라면 빠르게 전달해주세요!(카톡, 슬랙, 디코 등)
->
-
-## 🌴 Commit Convention
+## Commit Convention
| 태그 이름 | 설명 |
| --- | --- |
| [CHORE] | 코드 수정, 내부 파일 수정 |
@@ -61,7 +89,7 @@
-## ✨ Branch Strategy
+## Branch Strategy
- `main`, `develop`, `feature` 브랜치가 있습니다!
- **main**은 production용 브랜치입니다
- 실서비스용 ec2(**makers.operation.prod)**로 배포되도록 파이프라인이 구축되어 있습니다
@@ -72,61 +100,3 @@
- 각자 이슈에 대한 작업물의 브랜치입니다
- develop에 PR을 거쳐 merge 해주세요
-
-
-## 🗂 프로젝트 폴더 구조
-
-```
-📁 src
-|_ 📁 main
-|_ |_ 📁 common
-|_ |_ 📁 config
-|_ |_ 📁 controller
-|_ |_ 📁 dto
-|_ |_ 📁 entity
-|_ |_ 📁 exception
-|_ |_ 📁 repository
-|_ |_ 📁 security
-|_ |_ 📁 service
-|_ |_ 📁 util
-
-```
-
-
-
-## 🌴 Dependencies Module
-build.gradle
-```
-dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- implementation 'org.springframework.boot:spring-boot-starter-validation'
- implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.springframework.boot:spring-boot-starter-security'
- implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
- implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
-
- compileOnly 'org.projectlombok:lombok'
- runtimeOnly 'com.h2database:h2'
- runtimeOnly 'org.postgresql:postgresql'
- annotationProcessor 'org.projectlombok:lombok'
-
- // jwt
- implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
-
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
-
- // swagger
- implementation 'io.springfox:springfox-boot-starter:3.0.0'
- implementation 'io.springfox:springfox-swagger-ui:3.0.0'
-}
-
-```
-
-
-
-
-## 🏗 Architecture
-![image](https://user-images.githubusercontent.com/55437339/236621230-8d2dd581-c68d-44e9-bc0d-ea35dee08ebe.png)
-
diff --git a/build.gradle b/build.gradle
index a6ed1626..a8f27535 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,79 +1,55 @@
buildscript {
- ext {
- queryDslVersion = "5.0.0"
+ repositories {
+ mavenCentral()
}
}
plugins {
id 'java'
- id 'org.springframework.boot' version '2.7.4'
+ id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
- id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}
-group = 'org.sopt.makers'
-version = '0.0.1-SNAPSHOT'
-sourceCompatibility = '17'
+allprojects {
+ apply plugin: 'java'
+ apply plugin: 'org.springframework.boot'
+ apply plugin: 'io.spring.dependency-management'
-configurations {
- compileOnly {
- extendsFrom annotationProcessor
- }
-}
-
-repositories {
- mavenCentral()
-}
+ group = 'org.sopt.makers.operation'
+ version = '0.0.1-SNAPSHOT'
+ sourceCompatibility = '17'
-dependencies {
- implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- implementation 'org.springframework.boot:spring-boot-starter-validation'
- implementation 'org.springframework.boot:spring-boot-starter-web'
- implementation 'org.springframework.boot:spring-boot-starter-security'
- implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
- implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
- testImplementation 'junit:junit:4.13.1'
- testImplementation 'junit:junit:4.13.1'
-
- compileOnly 'org.projectlombok:lombok'
- runtimeOnly 'com.h2database:h2'
- runtimeOnly 'org.postgresql:postgresql'
- annotationProcessor 'org.projectlombok:lombok'
+ repositories {
+ mavenCentral()
+ }
- // jwt
- implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
- runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
+ configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+ }
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ dependencies {
+ // lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testAnnotationProcessor 'org.projectlombok:lombok'
+ testCompileOnly 'org.projectlombok:lombok'
- // swagger
- implementation 'io.springfox:springfox-boot-starter:3.0.0'
- implementation 'io.springfox:springfox-swagger-ui:3.0.0'
+ // test
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ }
- // gson
- implementation 'com.google.code.gson:gson:2.8.9'
+ tasks.named('test') {
+ useJUnitPlatform()
+ }
}
-tasks.named('test') {
- useJUnitPlatform()
+jar {
+ enabled = true
}
-def querydslDir = "$buildDir/generated/querydsl"
-
-querydsl {
- jpa = true
- querydslSourcesDir = querydslDir
-}
-sourceSets {
- main.java.srcDir querydslDir
-}
-compileQuerydsl{
- options.annotationProcessorPath = configurations.querydsl
-}
-configurations {
- compileOnly {
- extendsFrom annotationProcessor
- }
- querydsl.extendsFrom compileClasspath
-}
+bootJar {
+ enabled = false
+}
\ No newline at end of file
diff --git a/operation-api/build.gradle b/operation-api/build.gradle
new file mode 100644
index 00000000..2d20d26b
--- /dev/null
+++ b/operation-api/build.gradle
@@ -0,0 +1,24 @@
+jar {
+ enabled = false
+}
+
+bootJar {
+ enabled = true
+}
+
+dependencies {
+ // module
+ implementation project(path: ':operation-auth')
+ implementation project(path: ':operation-common')
+ implementation project(path: ':operation-domain')
+ implementation project(path: ':operation-external')
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+
+ // swagger
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
+
+}
\ No newline at end of file
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/OperationApplication.java b/operation-api/src/main/java/org/sopt/makers/operation/OperationApplication.java
new file mode 100644
index 00000000..ee82620f
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/OperationApplication.java
@@ -0,0 +1,14 @@
+package org.sopt.makers.operation;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(
+ scanBasePackageClasses = {AuthRoot.class, CommonRoot.class, DomainRoot.class, ExternalRoot.class}
+)
+public class OperationApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(OperationApplication.class, args);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApi.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApi.java
new file mode 100644
index 00000000..cf6c86cb
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApi.java
@@ -0,0 +1,41 @@
+package org.sopt.makers.operation.app.attendance.api;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.app.attendance.dto.request.LectureAttendRequest;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.http.ResponseEntity;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.NonNull;
+
+@Tag(name = "앱 출석 관련 API")
+public interface AppAttendanceApi {
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 출석 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> attend(
+ @RequestBody LectureAttendRequest request,
+ @Parameter(hidden = true) @NonNull Principal principal);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApiController.java
new file mode 100644
index 00000000..0415f03b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/api/AppAttendanceApiController.java
@@ -0,0 +1,36 @@
+package org.sopt.makers.operation.app.attendance.api;
+
+import static org.sopt.makers.operation.code.success.app.AttendanceSuccessCode.*;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.app.attendance.dto.request.LectureAttendRequest;
+import org.sopt.makers.operation.app.attendance.service.AppAttendanceService;
+import org.sopt.makers.operation.common.util.CommonUtils;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/app/attendances")
+public class AppAttendanceApiController implements AppAttendanceApi {
+
+ private final AppAttendanceService attendanceService;
+ private final CommonUtils utils;
+
+ @Override
+ @PostMapping("/attend")
+ public ResponseEntity> attend(@RequestBody LectureAttendRequest request, Principal principal) {
+ val memberId = utils.getMemberId(principal);
+ val response = attendanceService.attend(memberId, request);
+ return ApiResponseUtil.success(SUCCESS_ATTEND, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/request/LectureAttendRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/request/LectureAttendRequest.java
new file mode 100644
index 00000000..794dd562
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/request/LectureAttendRequest.java
@@ -0,0 +1,9 @@
+package org.sopt.makers.operation.app.attendance.dto.request;
+
+import lombok.NonNull;
+
+public record LectureAttendRequest(
+ long subLectureId,
+ @NonNull String code
+) {
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/response/LectureAttendResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/response/LectureAttendResponse.java
new file mode 100644
index 00000000..442b05b3
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/dto/response/LectureAttendResponse.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.app.attendance.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import lombok.Builder;
+
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+
+@Builder(access = PRIVATE)
+public record LectureAttendResponse(
+ long subLectureId
+) {
+ public static LectureAttendResponse of(SubAttendance subAttendance) {
+ return LectureAttendResponse.builder()
+ .subLectureId(subAttendance.getSubLecture().getId())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceService.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceService.java
new file mode 100644
index 00000000..3d0c5668
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceService.java
@@ -0,0 +1,8 @@
+package org.sopt.makers.operation.app.attendance.service;
+
+import org.sopt.makers.operation.app.attendance.dto.request.LectureAttendRequest;
+import org.sopt.makers.operation.app.attendance.dto.response.LectureAttendResponse;
+
+public interface AppAttendanceService {
+ LectureAttendResponse attend(long memberId, LectureAttendRequest request);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceServiceImpl.java
new file mode 100644
index 00000000..a72dee33
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/attendance/service/AppAttendanceServiceImpl.java
@@ -0,0 +1,115 @@
+package org.sopt.makers.operation.app.attendance.service;
+
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+import static org.sopt.makers.operation.code.failure.member.memberFailureCode.*;
+import static org.sopt.makers.operation.code.failure.subAttendance.subAttendanceFailureCode.*;
+import static org.sopt.makers.operation.code.failure.lecture.LectureFailureCode.*;
+import static org.sopt.makers.operation.code.failure.subLecture.subLectureFailureCode.*;
+
+import org.sopt.makers.operation.app.attendance.dto.request.LectureAttendRequest;
+import org.sopt.makers.operation.app.attendance.dto.response.LectureAttendResponse;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.attendance.repository.attendance.AttendanceRepository;
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.exception.SubLectureException;
+import org.sopt.makers.operation.exception.LectureException;
+import org.sopt.makers.operation.exception.MemberException;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+import org.sopt.makers.operation.lecture.repository.subLecture.SubLectureRepository;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class AppAttendanceServiceImpl implements AppAttendanceService {
+
+ private final AttendanceRepository attendanceRepository;
+ private final MemberRepository memberRepository;
+ private final SubLectureRepository subLectureRepository;
+ private final ValueConfig valueConfig;
+
+ @Override
+ @Transactional
+ public LectureAttendResponse attend(long playgroundId, LectureAttendRequest request) {
+ val subAttendance = getSubAttendance(request.subLectureId(), request.code(), playgroundId);
+ subAttendance.updateStatus(ATTENDANCE);
+ return LectureAttendResponse.of(subAttendance);
+ }
+
+ private SubAttendance getSubAttendance(long subLectureId, String code, long playgroundId) {
+ val subLecture = getSubLecture(subLectureId, code);
+ val attendance = getAttendance(subLecture, playgroundId);
+ return getSubAttendance(attendance, subLecture.getRound());
+ }
+
+ private SubLecture getSubLecture(long subLectureId, String code) {
+ val subLecture = findSubLecture(subLectureId);
+ checkSubLectureValidity(subLecture);
+ checkMatchedCode(subLecture, code);
+ return subLecture;
+ }
+
+ private SubLecture findSubLecture(long subLectureId) {
+ return subLectureRepository
+ .findById(subLectureId)
+ .orElseThrow(() -> new SubLectureException(INVALID_ATTENDANCE));
+ }
+
+ private void checkSubLectureValidity(SubLecture subLecture) {
+ checkSubLectureStarted(subLecture);
+ checkSubLectureEnded(subLecture);
+ }
+
+ private void checkSubLectureStarted(SubLecture subLecture) {
+ if (subLecture.isNotStarted()) {
+ throw new LectureException(NOT_STARTED_NTH_ATTENDANCE);
+ }
+ }
+
+ private void checkSubLectureEnded(SubLecture subLecture) {
+ val attendanceMinute = valueConfig.getATTENDANCE_MINUTE();
+ if (subLecture.isEnded(attendanceMinute)) {
+ throw new LectureException(ENDED_ATTENDANCE, subLecture.getRound());
+ }
+ }
+
+ private void checkMatchedCode(SubLecture subLecture, String code) {
+ if (subLecture.isMatchCode(code)) {
+ throw new SubLectureException(INVALID_CODE);
+ }
+ }
+
+ private Attendance getAttendance(SubLecture subLecture, long playgroundId) {
+ val lecture = subLecture.getLecture();
+ val generation = valueConfig.getGENERATION();
+ val member = findMember(playgroundId, generation);
+ return findAttendance(lecture, member);
+ }
+
+ private Member findMember(long playgroundId, int generation) {
+ return memberRepository
+ .getMemberByPlaygroundIdAndGeneration(playgroundId, generation)
+ .orElseThrow(() -> new MemberException(INVALID_MEMBER));
+ }
+
+ private Attendance findAttendance(Lecture lecture, Member member) {
+ return attendanceRepository.findByLectureAndMember(lecture, member)
+ .orElseThrow(() -> new LectureException(INVALID_ATTENDANCE));
+ }
+
+ private SubAttendance getSubAttendance(Attendance attendance, int round) {
+ return attendance.getSubAttendances().stream()
+ .filter(subAttendance -> subAttendance.isMatchRound(round))
+ .findFirst()
+ .orElseThrow(() -> new SubLectureException(INVALID_SUB_ATTENDANCE));
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApi.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApi.java
new file mode 100644
index 00000000..095c0a6f
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApi.java
@@ -0,0 +1,59 @@
+package org.sopt.makers.operation.app.lecture.api;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.NonNull;
+
+@Tag(name = "앱 세션 관련 API")
+public interface AppLectureApi {
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 진행 중인 세션 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "진행 중인 세션 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getTodayLecture(
+ @Parameter(hidden = true) @NonNull Principal principal);
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 출석 차수 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 차수 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getRound(@PathVariable long lectureId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApiController.java
new file mode 100644
index 00000000..4db6f636
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/api/AppLectureApiController.java
@@ -0,0 +1,38 @@
+package org.sopt.makers.operation.app.lecture.api;
+
+import static org.sopt.makers.operation.code.success.app.LectureSuccessCode.*;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.app.lecture.service.AppLectureService;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/app/lectures")
+public class AppLectureApiController implements AppLectureApi {
+
+ private final AppLectureService lectureService;
+
+ @Override
+ @GetMapping
+ public ResponseEntity> getTodayLecture(@NonNull Principal principal) {
+ val memberId = Long.parseLong(principal.getName());
+ val response = lectureService.getTodayLecture(memberId);
+ return ApiResponseUtil.success(SUCCESS_SINGLE_GET_LECTURE, response);
+ }
+
+ @Override
+ @GetMapping("/round/{lectureId}")
+ public ResponseEntity> getRound(@PathVariable long lectureId) {
+ val response = lectureService.getCurrentLectureRound(lectureId);
+ return ApiResponseUtil.success(SUCCESS_GET_LECTURE_ROUND, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureCurrentRoundResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureCurrentRoundResponse.java
new file mode 100644
index 00000000..231ea7c9
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureCurrentRoundResponse.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.app.lecture.dto.response;
+
+import lombok.Builder;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+
+import static lombok.AccessLevel.PRIVATE;
+
+@Builder(access = PRIVATE)
+public record LectureCurrentRoundResponse(
+ long id,
+ int round
+) {
+ public static LectureCurrentRoundResponse of(SubLecture subLecture){
+ return LectureCurrentRoundResponse.builder()
+ .id(subLecture.getId())
+ .round(subLecture.getRound())
+ .build();
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseType.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureResponseType.java
similarity index 58%
rename from src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseType.java
rename to operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureResponseType.java
index 9c5e7c33..ac9e5c3b 100644
--- a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureResponseType.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/LectureResponseType.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.dto.lecture;
+package org.sopt.makers.operation.app.lecture.dto.response;
public enum LectureResponseType {
NO_SESSION, HAS_ATTENDANCE, NO_ATTENDANCE
diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/TodayLectureResponse.java
similarity index 55%
rename from src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java
rename to operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/TodayLectureResponse.java
index 40685563..e5de11eb 100644
--- a/src/main/java/org/sopt/makers/operation/dto/lecture/TodayLectureResponseDTO.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/dto/response/TodayLectureResponse.java
@@ -1,19 +1,23 @@
-package org.sopt.makers.operation.dto.lecture;
-
-import lombok.Builder;
-import org.sopt.makers.operation.entity.AttendanceStatus;
-import org.sopt.makers.operation.entity.SubAttendance;
-import org.sopt.makers.operation.entity.lecture.Lecture;
+package org.sopt.makers.operation.app.lecture.dto.response;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
-@Builder
-public record TodayLectureResponseDTO(
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record TodayLectureResponse(
LectureResponseType type,
- Long id,
+ long id,
String location,
String name,
String startDate,
@@ -21,9 +25,8 @@ public record TodayLectureResponseDTO(
String message,
List attendances
) {
- public static TodayLectureResponseDTO of(LectureResponseType type, Lecture lecture, String message, List attendances) {
-
- return TodayLectureResponseDTO.builder()
+ public static TodayLectureResponse of(LectureResponseType type, Lecture lecture, String message, List attendances) {
+ return TodayLectureResponse.builder()
.type(type)
.id(lecture.getId())
.location(lecture.getPlace())
@@ -32,8 +35,21 @@ public static TodayLectureResponseDTO of(LectureResponseType type, Lecture lectu
.endDate(lecture.getEndDate().format(convertFormat()))
.message(message)
.attendances(attendances.stream()
- .map(subAttendance -> LectureGetResponseVO.of(subAttendance.getStatus(), subAttendance.getLastModifiedDate()))
- .collect(Collectors.toList()))
+ .map(subAttendance -> LectureGetResponseVO.of(subAttendance.getStatus(), subAttendance.getLastModifiedDate()))
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ public static TodayLectureResponse getEmptyResponse() {
+ return TodayLectureResponse.builder()
+ .type(LectureResponseType.NO_SESSION)
+ .id(0L)
+ .location("")
+ .name("")
+ .startDate("")
+ .endDate("")
+ .message("")
+ .attendances(Collections.emptyList())
.build();
}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureService.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureService.java
new file mode 100644
index 00000000..4a7f113e
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureService.java
@@ -0,0 +1,10 @@
+package org.sopt.makers.operation.app.lecture.service;
+
+import org.sopt.makers.operation.app.lecture.dto.response.LectureCurrentRoundResponse;
+import org.sopt.makers.operation.app.lecture.dto.response.TodayLectureResponse;
+
+public interface AppLectureService {
+
+ TodayLectureResponse getTodayLecture(long memberPlaygroundId);
+ LectureCurrentRoundResponse getCurrentLectureRound(long lectureId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureServiceImpl.java
new file mode 100644
index 00000000..7f5fe825
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/lecture/service/AppLectureServiceImpl.java
@@ -0,0 +1,190 @@
+package org.sopt.makers.operation.app.lecture.service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Collections;
+import java.util.List;
+
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+import static org.sopt.makers.operation.code.failure.lecture.LectureFailureCode.*;
+import static org.sopt.makers.operation.code.failure.subLecture.subLectureFailureCode.*;
+import static org.sopt.makers.operation.lecture.domain.Attribute.*;
+import static org.sopt.makers.operation.lecture.domain.LectureStatus.*;
+
+import org.sopt.makers.operation.app.lecture.dto.response.LectureCurrentRoundResponse;
+import org.sopt.makers.operation.app.lecture.dto.response.LectureResponseType;
+import org.sopt.makers.operation.app.lecture.dto.response.TodayLectureResponse;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.attendance.repository.attendance.AttendanceRepository;
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.exception.LectureException;
+import org.sopt.makers.operation.exception.SubLectureException;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+import org.sopt.makers.operation.lecture.repository.lecture.LectureRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class AppLectureServiceImpl implements AppLectureService {
+
+ private final LectureRepository lectureRepository;
+ private final AttendanceRepository attendanceRepository;
+ private final ValueConfig valueConfig;
+
+ @Override
+ public TodayLectureResponse getTodayLecture(long memberPlaygroundId) {
+ val attendances = attendanceRepository.findToday(memberPlaygroundId);
+ checkAttendancesSize(attendances);
+
+ if (attendances.isEmpty()) {
+ return TodayLectureResponse.getEmptyResponse();
+ }
+
+ val attendance = getNowAttendance(attendances);
+ val lecture = attendance.getLecture();
+ val responseType = getResponseType(lecture);
+ val message = getMessage(lecture.getAttribute());
+
+ if (responseType.equals(LectureResponseType.NO_ATTENDANCE) || lecture.isBefore()) {
+ return TodayLectureResponse.of(responseType, lecture, message, Collections.emptyList());
+ }
+
+ val subAttendances = attendance.getSubAttendances();
+
+ if (lecture.isFirst()) {
+ return getTodayFirstLectureResponse(subAttendances.get(0), responseType, lecture);
+ }
+
+ return getTodaySecondLectureResponse(subAttendances, responseType, lecture);
+ }
+
+ private void checkAttendancesSize(List attendances) {
+ if (attendances.size() > valueConfig.getSUB_LECTURE_MAX_ROUND()) {
+ throw new LectureException(INVALID_COUNT_SESSION);
+ }
+ }
+
+ private boolean checkOnAttendanceAbsence(SubLecture subLecture, SubAttendance subAttendance) {
+ val isOnAttendanceCheck = LocalDateTime.now().isBefore(subLecture.getStartAt().plusMinutes(10));
+ return isOnAttendanceCheck && subAttendance.getStatus().equals(ABSENT);
+ }
+
+ private Attendance getNowAttendance(List attendances) {
+ val index = getAttendanceIndex();
+ return attendances.get(index);
+ }
+
+ private int getAttendanceIndex() {
+ return (LocalDateTime.now().getHour() >= 16) ? 1 : 0;
+ }
+
+ private LectureResponseType getResponseType(Lecture lecture) {
+ val attribute = lecture.getAttribute();
+ return attribute.equals(ETC) ? LectureResponseType.NO_ATTENDANCE : LectureResponseType.HAS_ATTENDANCE;
+ }
+
+ private String getMessage(Attribute attribute) {
+ return switch (attribute) {
+ case SEMINAR -> valueConfig.getSEMINAR_MESSAGE();
+ case EVENT -> valueConfig.getEVENT_MESSAGE();
+ case ETC -> valueConfig.getETC_MESSAGE();
+ };
+ }
+
+ private TodayLectureResponse getTodayFirstLectureResponse(SubAttendance subAttendance, LectureResponseType responseType, Lecture lecture) {
+ val subLecture = subAttendance.getSubLecture();
+ val message = getMessage(lecture.getAttribute());
+ if (checkOnAttendanceAbsence(subLecture, subAttendance)) {
+ return TodayLectureResponse.of(responseType, lecture, message, Collections.emptyList());
+ }
+ return TodayLectureResponse.of(responseType, lecture, message, Collections.singletonList(subAttendance));
+ }
+
+ private TodayLectureResponse getTodaySecondLectureResponse(
+ List subAttendances,
+ LectureResponseType responseType,
+ Lecture lecture
+ ) {
+ val subAttendance = subAttendances.get(1);
+ val subLecture = subAttendance.getSubLecture();
+ val message = getMessage(lecture.getAttribute());
+ if (checkOnAttendanceAbsence(subLecture, subAttendance)) {
+ return TodayLectureResponse.of(responseType, lecture, message, Collections.singletonList(subAttendances.get(0)));
+ }
+ return TodayLectureResponse.of(responseType, lecture, message, subAttendances);
+ }
+
+ @Override
+ public LectureCurrentRoundResponse getCurrentLectureRound(long lectureId) {
+ val lecture = findLecture(lectureId);
+ val subLecture = getSubLecture(lecture);
+ checkLectureExist(lecture);
+ checkLectureBefore(lecture);
+ checkEndAttendance(subLecture);
+ checkLectureEnd(lecture);
+ return LectureCurrentRoundResponse.of(subLecture);
+ }
+
+ private Lecture findLecture(Long id) {
+ return lectureRepository.findById(id)
+ .orElseThrow(() -> new LectureException(INVALID_LECTURE));
+ }
+
+ private SubLecture getSubLecture(Lecture lecture) {
+ val status = lecture.getLectureStatus();
+ val round = status.equals(FIRST) ? 1 : 2;
+ return getSubLecture(lecture, round);
+ }
+
+ private SubLecture getSubLecture(Lecture lecture, int round) {
+ return lecture.getSubLectures().stream()
+ .filter(l -> l.getRound() == round)
+ .findFirst()
+ .orElseThrow(() -> new SubLectureException(NO_SUB_LECTURE_EQUAL_ROUND));
+ }
+
+ private void checkLectureExist(Lecture lecture) {
+ val today = LocalDate.now();
+ val startOfDay = today.atStartOfDay();
+ val endOfDay = LocalDateTime.of(today, LocalTime.MAX);
+ val startAt = lecture.getStartDate();
+ if (startAt.isBefore(startOfDay) || startAt.isAfter(endOfDay)) {
+ throw new LectureException(NO_SESSION);
+ }
+ }
+
+ private void checkLectureBefore(Lecture lecture) {
+ if (lecture.isBefore()) {
+ throw new LectureException(NOT_STARTED_ATTENDANCE);
+ }
+ }
+
+ private void checkEndAttendance(SubLecture subLecture) {
+ if (isEndAttendance(subLecture)) {
+ throw new LectureException(ENDED_ATTENDANCE, subLecture.getRound());
+ }
+ }
+
+ private boolean isEndAttendance(SubLecture subLecture) {
+ val status = subLecture.getLecture().getLectureStatus();
+ if (LocalDateTime.now().isAfter(subLecture.getStartAt().plusMinutes(10))) {
+ return status.equals(FIRST) || status.equals(SECOND);
+ }
+ return false;
+ }
+
+ private void checkLectureEnd(Lecture lecture) {
+ if (lecture.isEnd()) {
+ throw new LectureException(END_LECTURE);
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApi.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApi.java
new file mode 100644
index 00000000..88771cb8
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApi.java
@@ -0,0 +1,57 @@
+package org.sopt.makers.operation.app.member.api;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.http.ResponseEntity;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import lombok.NonNull;
+
+public interface AppMemberApi {
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 전체 출석 정보 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "전체 출석 정보 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getMemberTotalAttendance(
+ @Parameter(hidden = true) @NonNull Principal principal);
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 출석 점수 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 점수 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getScore(
+ @Parameter(hidden = true) @NonNull Principal principal);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApiController.java
new file mode 100644
index 00000000..caad302b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/api/AppMemberApiController.java
@@ -0,0 +1,43 @@
+package org.sopt.makers.operation.app.member.api;
+
+import static org.sopt.makers.operation.code.success.app.MemberSuccessCode.*;
+
+import java.security.Principal;
+
+import org.sopt.makers.operation.app.member.service.AppMemberService;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.common.util.CommonUtils;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/app/members")
+public class AppMemberApiController implements AppMemberApi {
+
+ private final AppMemberService memberService;
+ private final CommonUtils utils;
+
+ @Override
+ @GetMapping("/attendances")
+ public ResponseEntity> getMemberTotalAttendance(@NonNull Principal principal) {
+ val memberId = utils.getMemberId(principal);
+ val response = memberService.getMemberTotalAttendance(memberId);
+ return ApiResponseUtil.success(SUCCESS_GET_TOTAL_ATTENDANCE, response);
+ }
+
+ @Override
+ @GetMapping("/score")
+ public ResponseEntity> getScore(@NonNull Principal principal) {
+ val memberId = utils.getMemberId(principal);
+ val response = memberService.getMemberScore(memberId);
+ return ApiResponseUtil.success(SUCCESS_GET_ATTENDANCE_SCORE, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceStatusListVO.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceStatusListVO.java
new file mode 100644
index 00000000..fcde3507
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceStatusListVO.java
@@ -0,0 +1,32 @@
+package org.sopt.makers.operation.app.member.dto.response;
+
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.member.domain.Member;
+
+import lombok.Builder;
+
+@Builder
+public record AttendanceStatusListVO(
+ int attendance,
+ int absent,
+ int tardy,
+ int participate
+) {
+
+ public static AttendanceStatusListVO of(Member member) {
+ return AttendanceStatusListVO.builder()
+ .attendance(getCount(member, ATTENDANCE))
+ .absent(getCount(member, ABSENT))
+ .tardy(getCount(member, TARDY))
+ .participate(getCount(member, PARTICIPATE))
+ .build();
+ }
+
+ private static int getCount(Member member, AttendanceStatus status) {
+ return (int)member.getAttendances().stream()
+ .filter(attendance -> attendance.getStatus().equals(status))
+ .count();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalResponseDTO.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalResponseDTO.java
new file mode 100644
index 00000000..1bf8f22a
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalResponseDTO.java
@@ -0,0 +1,34 @@
+package org.sopt.makers.operation.app.member.dto.response;
+
+import java.util.List;
+
+import lombok.Builder;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.member.domain.Member;
+
+import static lombok.AccessLevel.PRIVATE;
+
+@Builder(access = PRIVATE)
+public record AttendanceTotalResponseDTO(
+ Part part,
+ int generation,
+ String name,
+ float score,
+ AttendanceStatusListVO total,
+ List attendances
+) {
+ public static AttendanceTotalResponseDTO of(Member member, List attendances){
+ return AttendanceTotalResponseDTO.builder()
+ .part(member.getPart())
+ .generation(member.getGeneration())
+ .name(member.getName())
+ .score(member.getScore())
+ .total(getTotal(member))
+ .attendances(attendances)
+ .build();
+ }
+
+ private static AttendanceStatusListVO getTotal(Member member) {
+ return AttendanceStatusListVO.of(member);
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/dto/attendance/AttendanceTotalVO.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalVO.java
similarity index 77%
rename from src/main/java/org/sopt/makers/operation/dto/attendance/AttendanceTotalVO.java
rename to operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalVO.java
index 0d576ca2..e9754509 100644
--- a/src/main/java/org/sopt/makers/operation/dto/attendance/AttendanceTotalVO.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/AttendanceTotalVO.java
@@ -1,11 +1,11 @@
-package org.sopt.makers.operation.dto.attendance;
-
-import org.sopt.makers.operation.entity.Attendance;
-import org.sopt.makers.operation.entity.AttendanceStatus;
-import org.sopt.makers.operation.entity.lecture.Attribute;
+package org.sopt.makers.operation.app.member.dto.response;
import java.time.format.DateTimeFormatter;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+
public record AttendanceTotalVO(
Attribute attribute,
String name,
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/MemberScoreGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/MemberScoreGetResponse.java
new file mode 100644
index 00000000..6dfc6601
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/dto/response/MemberScoreGetResponse.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.app.member.dto.response;
+
+import static lombok.AccessLevel.PRIVATE;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record MemberScoreGetResponse(
+ float score
+) {
+ public static MemberScoreGetResponse of(float score){
+ return MemberScoreGetResponse.builder()
+ .score(score)
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberService.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberService.java
new file mode 100644
index 00000000..db0afb33
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberService.java
@@ -0,0 +1,9 @@
+package org.sopt.makers.operation.app.member.service;
+
+import org.sopt.makers.operation.app.member.dto.response.AttendanceTotalResponseDTO;
+import org.sopt.makers.operation.app.member.dto.response.MemberScoreGetResponse;
+
+public interface AppMemberService {
+ AttendanceTotalResponseDTO getMemberTotalAttendance(Long playGroundId);
+ MemberScoreGetResponse getMemberScore(Long playGroundId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberServiceImpl.java
new file mode 100644
index 00000000..f56a849a
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/member/service/AppMemberServiceImpl.java
@@ -0,0 +1,65 @@
+package org.sopt.makers.operation.app.member.service;
+
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+import static org.sopt.makers.operation.code.failure.member.memberFailureCode.*;
+import static org.sopt.makers.operation.lecture.domain.Attribute.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.app.member.dto.response.AttendanceTotalResponseDTO;
+import org.sopt.makers.operation.app.member.dto.response.AttendanceTotalVO;
+import org.sopt.makers.operation.app.member.dto.response.MemberScoreGetResponse;
+import org.sopt.makers.operation.attendance.repository.attendance.AttendanceRepository;
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.exception.MemberException;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class AppMemberServiceImpl implements AppMemberService {
+ private final MemberRepository memberRepository;
+ private final AttendanceRepository attendanceRepository;
+ private final ValueConfig valueConfig;
+
+ @Override
+ public AttendanceTotalResponseDTO getMemberTotalAttendance(Long playGroundId) {
+ val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, valueConfig.getGENERATION())
+ .orElseThrow(() -> new MemberException(INVALID_MEMBER));
+
+ val attendances = findAttendances(member);
+
+ val filteredAttendances = filterEtcNoAppearance(attendances);
+
+ return AttendanceTotalResponseDTO.of(member, filteredAttendances);
+ }
+
+ @Override
+ public MemberScoreGetResponse getMemberScore(Long playGroundId) {
+ val member = memberRepository.getMemberByPlaygroundIdAndGeneration(playGroundId, valueConfig.getGENERATION())
+ .orElseThrow(() -> new MemberException(INVALID_MEMBER));
+
+ return MemberScoreGetResponse.of(member.getScore());
+ }
+
+ private List filterEtcNoAppearance(List attendances) {
+ return attendances.stream()
+ .filter(attendanceTotalVO ->
+ !(attendanceTotalVO.attribute().equals(ETC)
+ && attendanceTotalVO.status().equals(NOT_PARTICIPATE))
+ )
+ .toList();
+ }
+
+ private List findAttendances(Member member) {
+ return attendanceRepository.findAttendanceByMemberId(member.getId())
+ .stream().map(AttendanceTotalVO::getTotalAttendanceVO)
+ .toList();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApi.java b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApi.java
new file mode 100644
index 00000000..9fcc6627
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApi.java
@@ -0,0 +1,37 @@
+package org.sopt.makers.operation.app.schedule.api;
+
+import java.time.LocalDateTime;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+
+public interface ScheduleApi {
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 일정 리스트 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "일정 리스트 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getSchedules(
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start,
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApiController.java
new file mode 100644
index 00000000..542ce85c
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/api/ScheduleApiController.java
@@ -0,0 +1,37 @@
+package org.sopt.makers.operation.app.schedule.api;
+
+import static org.sopt.makers.operation.code.success.app.ScheduleSuccessCode.*;
+import static org.springframework.format.annotation.DateTimeFormat.ISO.*;
+
+import java.time.LocalDateTime;
+
+import org.sopt.makers.operation.app.schedule.service.ScheduleService;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/app/schedules")
+public class ScheduleApiController implements ScheduleApi {
+
+ private final ScheduleService scheduleService;
+
+ @Override
+ @GetMapping
+ public ResponseEntity> getSchedules(
+ @RequestParam @DateTimeFormat(iso = DATE_TIME) LocalDateTime start,
+ @RequestParam @DateTimeFormat(iso = DATE_TIME) LocalDateTime end
+ ) {
+ val response = scheduleService.getSchedules(start, end);
+ return ApiResponseUtil.success(SUCCESS_GET_SCHEDULES, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/dto/response/ScheduleListGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/dto/response/ScheduleListGetResponse.java
new file mode 100644
index 00000000..c2f38087
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/dto/response/ScheduleListGetResponse.java
@@ -0,0 +1,68 @@
+package org.sopt.makers.operation.app.schedule.dto.response;
+
+import static java.time.format.TextStyle.*;
+import static java.util.Locale.*;
+import static lombok.AccessLevel.*;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+import org.sopt.makers.operation.lecture.domain.Attribute;
+import org.sopt.makers.operation.schedule.domain.Schedule;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record ScheduleListGetResponse(
+ List dates
+) {
+
+ public static ScheduleListGetResponse of(Map> scheduleMap) {
+ return ScheduleListGetResponse.builder()
+ .dates(getDates(scheduleMap))
+ .build();
+ }
+
+ private static List getDates(Map> scheduleMap) {
+ return scheduleMap.keySet().stream().sorted()
+ .map(key -> DateResponse.of(key, scheduleMap.get(key)))
+ .toList();
+ }
+
+ @Builder(access = PRIVATE)
+ record DateResponse(
+ String date,
+ String dayOfWeek,
+ List schedules
+ ) {
+
+ private static DateResponse of(LocalDate date, List schedules) {
+ return DateResponse.builder()
+ .date(date.toString())
+ .dayOfWeek(date.getDayOfWeek().getDisplayName(SHORT, KOREAN))
+ .schedules(schedules.stream().map(ScheduleResponse::of).toList())
+ .build();
+ }
+ }
+
+ @Builder(access = PRIVATE)
+ record ScheduleResponse(
+ long scheduleId,
+ String startDate,
+ String endDate,
+ Attribute attribute,
+ String title
+ ) {
+
+ private static ScheduleResponse of(Schedule schedule) {
+ return ScheduleResponse.builder()
+ .scheduleId(schedule.getId())
+ .startDate(schedule.getStartDate().toString())
+ .endDate(schedule.getEndDate().toString())
+ .attribute(schedule.getAttribute())
+ .title(schedule.getTitle())
+ .build();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleService.java b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleService.java
new file mode 100644
index 00000000..3f84203a
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleService.java
@@ -0,0 +1,9 @@
+package org.sopt.makers.operation.app.schedule.service;
+
+import java.time.LocalDateTime;
+
+import org.sopt.makers.operation.app.schedule.dto.response.ScheduleListGetResponse;
+
+public interface ScheduleService {
+ ScheduleListGetResponse getSchedules(LocalDateTime startAt, LocalDateTime endAt);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleServiceImpl.java
new file mode 100644
index 00000000..74cb962b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/app/schedule/service/ScheduleServiceImpl.java
@@ -0,0 +1,95 @@
+package org.sopt.makers.operation.app.schedule.service;
+
+import static java.time.temporal.ChronoUnit.*;
+import static org.sopt.makers.operation.code.failure.ScheduleFailureCode.*;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.schedule.domain.Schedule;
+import org.sopt.makers.operation.schedule.repository.ScheduleRepository;
+import org.sopt.makers.operation.exception.ScheduleException;
+import org.sopt.makers.operation.app.schedule.dto.response.ScheduleListGetResponse;
+import org.springframework.stereotype.Service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+public class ScheduleServiceImpl implements ScheduleService {
+
+ private final ScheduleRepository scheduleRepository;
+
+ private final ValueConfig valueConfig;
+
+ @Override
+ public ScheduleListGetResponse getSchedules(LocalDateTime startAt, LocalDateTime endAt) {
+ val schedules = scheduleRepository.findBetween(startAt, endAt);
+ val scheduleMap = getClassifiedMap(schedules, startAt, endAt);
+ return ScheduleListGetResponse.of(scheduleMap);
+ }
+
+ private Map> getClassifiedMap(
+ List schedules,
+ LocalDateTime startAt,
+ LocalDateTime endAt
+ ) {
+ val scheduleMap = getInitializedMap(startAt, endAt);
+ schedules.forEach(schedule -> putScheduleToMap(scheduleMap, schedule));
+ return scheduleMap;
+ }
+
+ private Map> getInitializedMap(LocalDateTime startAt, LocalDateTime endAt) {
+ //TODO: 클라이언트 개발 시간 리소스 절약을 위해 해당 메소드 활용, 가능한 일정이 존재하는 날짜만 key로 가지는 HashMap로 변경 요망
+ val duration = getDuration(startAt, endAt);
+ return IntStream.range(0, duration)
+ .mapToObj(startAt::plusDays)
+ .collect(Collectors.toMap(LocalDateTime::toLocalDate, date -> new ArrayList<>()));
+ }
+
+ private int getDuration(LocalDateTime startAt, LocalDateTime endAt) {
+ val duration = Duration.between(startAt, endAt).toDays() + 1;
+ val minDuration = valueConfig.getMIN_SCHEDULE_DURATION();
+ val maxDuration = valueConfig.getMAX_SCHEDULE_DURATION();
+
+ if (duration < minDuration || duration > maxDuration) {
+ throw new ScheduleException(INVALID_DATE_PERM);
+ }
+
+ return (int)duration;
+ }
+
+ private void putScheduleToMap(Map> scheduleMap, Schedule schedule) {
+ val duration = DAYS.between(schedule.getStartDate(), schedule.getEndDate());
+ val dayDuration = valueConfig.getDAY_DURATION();
+ val twoDaysDuration = valueConfig.getTWO_DAYS_DURATION();
+
+ scheduleMap.computeIfAbsent(schedule.getStartDate().toLocalDate(), k -> new ArrayList<>()).add(schedule);
+
+ if (duration >= dayDuration) {
+ scheduleMap.computeIfAbsent(schedule.getEndDate().toLocalDate(), k -> new ArrayList<>()).add(schedule);
+ if (duration >= twoDaysDuration) {
+ putScheduleMapBetween(scheduleMap, schedule, (int)duration);
+ }
+ }
+ }
+
+ private void putScheduleMapBetween(Map> scheduleMap, Schedule schedule, int duration) {
+ Stream.iterate(1, i -> i + 1).limit(duration - 1)
+ .forEach(i -> putScheduleAtDayCount(scheduleMap, schedule, i));
+ }
+
+ private void putScheduleAtDayCount(Map> scheduleMap, Schedule schedule, int dayCount) {
+ val date = schedule.getStartDate().plusDays(dayCount).toLocalDate();
+ scheduleMap.computeIfAbsent(date, k -> new ArrayList<>()).add(schedule);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/common/config/SwaggerConfig.java b/operation-api/src/main/java/org/sopt/makers/operation/common/config/SwaggerConfig.java
new file mode 100644
index 00000000..8d8c0134
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/common/config/SwaggerConfig.java
@@ -0,0 +1,29 @@
+package org.sopt.makers.operation.common.config;
+
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@SecurityScheme(
+ name = "Authorization",
+ type = SecuritySchemeType.HTTP,
+ in = SecuritySchemeIn.HEADER,
+ bearerFormat = "JWT",
+ scheme = "Bearer"
+)
+@Configuration
+public class SwaggerConfig {
+ @Bean
+ public OpenAPI api() {
+ Info info = new Info()
+ .title("Makers Operation API Docs")
+ .version("v2.0")
+ .description("운영 프로덕트 API 명세서 입니다.");
+ return new OpenAPI()
+ .info(info);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/sopt/makers/operation/config/TimezoneConfig.java b/operation-api/src/main/java/org/sopt/makers/operation/common/config/TimezoneConfig.java
similarity index 53%
rename from src/main/java/org/sopt/makers/operation/config/TimezoneConfig.java
rename to operation-api/src/main/java/org/sopt/makers/operation/common/config/TimezoneConfig.java
index f14faffe..bbf0503d 100644
--- a/src/main/java/org/sopt/makers/operation/config/TimezoneConfig.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/common/config/TimezoneConfig.java
@@ -1,13 +1,13 @@
-package org.sopt.makers.operation.config;
+package org.sopt.makers.operation.common.config;
import java.util.TimeZone;
-import javax.annotation.PostConstruct;
-
import org.springframework.context.annotation.Configuration;
+import jakarta.annotation.PostConstruct;
+
@Configuration
-public class TimezoneConfig {
+public class TimezoneConfig { //TODO: 타임 체크 로컬에서 UTF로 바꿔서 체크
@PostConstruct
public void init() {
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/common/handler/ErrorHandler.java b/operation-api/src/main/java/org/sopt/makers/operation/common/handler/ErrorHandler.java
new file mode 100644
index 00000000..5a4093f2
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/common/handler/ErrorHandler.java
@@ -0,0 +1,77 @@
+package org.sopt.makers.operation.common.handler;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.exception.AdminFailureException;
+import org.sopt.makers.operation.exception.AlarmException;
+import org.sopt.makers.operation.exception.AttendanceException;
+import org.sopt.makers.operation.exception.DateTimeParseCustomException;
+import org.sopt.makers.operation.exception.LectureException;
+import org.sopt.makers.operation.exception.MemberException;
+import org.sopt.makers.operation.exception.ScheduleException;
+import org.sopt.makers.operation.exception.SubLectureException;
+import org.sopt.makers.operation.exception.TokenException;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@RestControllerAdvice
+public class ErrorHandler {
+
+ @ExceptionHandler(AdminFailureException.class)
+ public ResponseEntity> authFailureException(AdminFailureException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(TokenException.class)
+ public ResponseEntity> tokenException(TokenException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(ScheduleException.class)
+ public ResponseEntity> scheduleException(ScheduleException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(MemberException.class)
+ public ResponseEntity> memberException(MemberException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(LectureException.class)
+ public ResponseEntity> lectureException(LectureException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(SubLectureException.class)
+ public ResponseEntity> subLectureException(SubLectureException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(DateTimeParseCustomException.class)
+ public ResponseEntity> dateTimeParseException(DateTimeParseCustomException ex) {
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(AlarmException.class)
+ public ResponseEntity> alarmException(AlarmException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+ @ExceptionHandler(AttendanceException.class)
+ public ResponseEntity> attendanceException(AttendanceException ex) {
+ log.error(ex.getMessage());
+ return ApiResponseUtil.failure(ex.getFailureCode());
+ }
+
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/common/util/CommonUtils.java b/operation-api/src/main/java/org/sopt/makers/operation/common/util/CommonUtils.java
new file mode 100644
index 00000000..40b7f0e7
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/common/util/CommonUtils.java
@@ -0,0 +1,14 @@
+package org.sopt.makers.operation.common.util;
+
+import java.security.Principal;
+
+import org.springframework.context.annotation.Configuration;
+
+import lombok.NonNull;
+
+@Configuration
+public class CommonUtils {
+ public long getMemberId(@NonNull Principal principal) {
+ return Long.parseLong(principal.getName());
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/util/Cookie.java b/operation-api/src/main/java/org/sopt/makers/operation/common/util/Cookie.java
similarity index 94%
rename from src/main/java/org/sopt/makers/operation/util/Cookie.java
rename to operation-api/src/main/java/org/sopt/makers/operation/common/util/Cookie.java
index 14b0f300..849bed17 100644
--- a/src/main/java/org/sopt/makers/operation/util/Cookie.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/common/util/Cookie.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.util;
+package org.sopt.makers.operation.common.util;
import lombok.val;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java b/operation-api/src/main/java/org/sopt/makers/operation/scheduler/LectureScheduler.java
similarity index 54%
rename from src/main/java/org/sopt/makers/operation/controller/ScheduleController.java
rename to operation-api/src/main/java/org/sopt/makers/operation/scheduler/LectureScheduler.java
index 5b7a673e..16a914eb 100644
--- a/src/main/java/org/sopt/makers/operation/controller/ScheduleController.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/scheduler/LectureScheduler.java
@@ -1,18 +1,18 @@
-package org.sopt.makers.operation.controller;
+package org.sopt.makers.operation.scheduler;
-import org.sopt.makers.operation.service.LectureService;
+import org.sopt.makers.operation.web.lecture.service.WebLectureService;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
-@RestController
+@Component
@EnableScheduling
@RequiredArgsConstructor
-public class ScheduleController {
+public class LectureScheduler {
- private final LectureService lectureService;
+ private final WebLectureService lectureService;
@Scheduled(cron = "0 0 0 ? * SUN")
public void endLecture() {
diff --git a/src/main/java/org/sopt/makers/operation/controller/HealthCheckController.java b/operation-api/src/main/java/org/sopt/makers/operation/test/HealthCheckController.java
similarity index 62%
rename from src/main/java/org/sopt/makers/operation/controller/HealthCheckController.java
rename to operation-api/src/main/java/org/sopt/makers/operation/test/HealthCheckController.java
index 12bfd6af..efebcb82 100644
--- a/src/main/java/org/sopt/makers/operation/controller/HealthCheckController.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/test/HealthCheckController.java
@@ -1,15 +1,14 @@
-package org.sopt.makers.operation.controller;
+package org.sopt.makers.operation.test;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import springfox.documentation.annotations.ApiIgnore;
-
@RestController
-@ApiIgnore
+@RequestMapping("/api/v1/test")
public class HealthCheckController {
- @GetMapping("/")
+ @GetMapping
public String healthCheck() {
return "Hello Operation!";
}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApi.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApi.java
new file mode 100644
index 00000000..4b222fdc
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApi.java
@@ -0,0 +1,75 @@
+package org.sopt.makers.operation.web.admin.api;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.web.admin.dto.request.LoginRequest;
+import org.sopt.makers.operation.web.admin.dto.request.SignUpRequest;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+
+public interface AdminApi {
+
+ @Operation(
+ summary = "회원가입 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "회원가입 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> signup(@RequestBody SignUpRequest signUpRequestDTO);
+
+
+ @Operation(
+ summary = "로그인 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "로그인 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> login(@RequestBody LoginRequest userLoginRequestDTO);
+
+
+ @Operation(
+ security = @SecurityRequirement(name = "Authorization"),
+ summary = "앱 내 일정 리스트 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "일정 리스트 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> refresh(@CookieValue String refreshToken);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApiController.java
new file mode 100644
index 00000000..8273df83
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/api/AdminApiController.java
@@ -0,0 +1,52 @@
+package org.sopt.makers.operation.web.admin.api;
+
+import static org.sopt.makers.operation.code.success.web.AdminSuccessCode.*;
+
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.common.util.Cookie;
+import org.sopt.makers.operation.web.admin.dto.request.LoginRequest;
+import org.sopt.makers.operation.web.admin.dto.request.SignUpRequest;
+import org.sopt.makers.operation.web.admin.service.AdminService;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/auth")
+@ComponentScan("operation-business")
+public class AdminApiController implements AdminApi {
+
+ private final AdminService authService;
+ private final Cookie cookie;
+
+ @Override
+ @PostMapping("/signup")
+ public ResponseEntity> signup(SignUpRequest signUpRequestDTO) {
+ val response = authService.signUp(signUpRequestDTO);
+ return ApiResponseUtil.success(SUCCESS_SIGN_UP, response);
+ }
+
+ @Override
+ @PostMapping("/login")
+ public ResponseEntity> login(LoginRequest userLoginRequestDTO) {
+ val response = authService.login(userLoginRequestDTO);
+ val headers = cookie.setRefreshToken(response.refreshToken());
+ return ApiResponseUtil.success(SUCCESS_LOGIN_UP, headers, response.loginResponseVO());
+ }
+
+ @Override
+ @PatchMapping("/refresh")
+ public ResponseEntity> refresh(String refreshToken) {
+ val response = authService.refresh(refreshToken);
+ val headers = cookie.setRefreshToken(response.refreshToken());
+ return ApiResponseUtil.success(SUCCESS_GET_REFRESH_TOKEN, headers, response.accessToken());
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/LoginRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/LoginRequest.java
new file mode 100644
index 00000000..d612c34f
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/LoginRequest.java
@@ -0,0 +1,7 @@
+package org.sopt.makers.operation.web.admin.dto.request;
+
+public record LoginRequest(
+ String email,
+ String password
+) {
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/SignUpRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/SignUpRequest.java
new file mode 100644
index 00000000..87f77268
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/request/SignUpRequest.java
@@ -0,0 +1,21 @@
+package org.sopt.makers.operation.web.admin.dto.request;
+
+import org.sopt.makers.operation.admin.domain.Admin;
+import org.sopt.makers.operation.admin.domain.Role;
+
+public record SignUpRequest(
+ String email,
+ String password,
+ String name,
+ Role role
+) {
+
+ public Admin toEntity() {
+ return Admin.builder()
+ .email(this.email)
+ .password(this.password)
+ .name(this.name)
+ .role(this.role)
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/LoginResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/LoginResponse.java
new file mode 100644
index 00000000..6aa415e7
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/LoginResponse.java
@@ -0,0 +1,39 @@
+package org.sopt.makers.operation.web.admin.dto.response;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+
+import org.sopt.makers.operation.admin.domain.Admin;
+import org.sopt.makers.operation.admin.domain.AdminStatus;
+
+@Builder(access = AccessLevel.PRIVATE)
+public record LoginResponse(
+ LoginResponseVO loginResponseVO,
+ String refreshToken
+) {
+
+ public static LoginResponse of(Admin admin, String accessToken) {
+ return builder()
+ .loginResponseVO(LoginResponseVO.of(admin, accessToken))
+ .refreshToken(admin.getRefreshToken())
+ .build();
+ }
+
+ @Builder
+ record LoginResponseVO(
+ Long id,
+ String name,
+ AdminStatus adminStatus,
+ String accessToken
+ ) {
+
+ public static LoginResponseVO of(Admin admin, String accessToken) {
+ return builder()
+ .id(admin.getId())
+ .name(admin.getName())
+ .adminStatus(admin.getStatus())
+ .accessToken(accessToken)
+ .build();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/RefreshResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/RefreshResponse.java
new file mode 100644
index 00000000..a66ef05b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/RefreshResponse.java
@@ -0,0 +1,13 @@
+package org.sopt.makers.operation.web.admin.dto.response;
+
+import org.sopt.makers.operation.admin.domain.Admin;
+
+public record RefreshResponse(
+ String accessToken,
+ String refreshToken
+) {
+
+ public static RefreshResponse of(String accessToken, Admin admin) {
+ return new RefreshResponse(accessToken, admin.getRefreshToken());
+ }
+}
\ No newline at end of file
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/SignUpResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/SignUpResponse.java
new file mode 100644
index 00000000..ad36d462
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/dto/response/SignUpResponse.java
@@ -0,0 +1,25 @@
+package org.sopt.makers.operation.web.admin.dto.response;
+
+import org.sopt.makers.operation.admin.domain.Admin;
+import org.sopt.makers.operation.admin.domain.Role;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+
+@Builder(access = AccessLevel.PRIVATE)
+public record SignUpResponse(
+ Long id,
+ String email,
+ String name,
+ Role role
+) {
+
+ public static SignUpResponse of(Admin admin) {
+ return SignUpResponse.builder()
+ .id(admin.getId())
+ .email(admin.getEmail())
+ .name(admin.getName())
+ .role(admin.getRole())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminService.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminService.java
new file mode 100644
index 00000000..241b6f9b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminService.java
@@ -0,0 +1,13 @@
+package org.sopt.makers.operation.web.admin.service;
+
+import org.sopt.makers.operation.web.admin.dto.request.LoginRequest;
+import org.sopt.makers.operation.web.admin.dto.request.SignUpRequest;
+import org.sopt.makers.operation.web.admin.dto.response.LoginResponse;
+import org.sopt.makers.operation.web.admin.dto.response.RefreshResponse;
+import org.sopt.makers.operation.web.admin.dto.response.SignUpResponse;
+
+public interface AdminService {
+ SignUpResponse signUp(SignUpRequest request);
+ LoginResponse login(LoginRequest request);
+ RefreshResponse refresh(String refreshToken);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminServiceImpl.java
new file mode 100644
index 00000000..f93e4861
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/admin/service/AdminServiceImpl.java
@@ -0,0 +1,108 @@
+package org.sopt.makers.operation.web.admin.service;
+
+import static org.sopt.makers.operation.code.failure.member.memberFailureCode.*;
+import static org.sopt.makers.operation.code.failure.admin.AdminFailureCode.*;
+
+import org.sopt.makers.operation.admin.domain.Admin;
+import org.sopt.makers.operation.authentication.AdminAuthentication;
+import org.sopt.makers.operation.exception.AdminFailureException;
+import org.sopt.makers.operation.jwt.JwtTokenProvider;
+import org.sopt.makers.operation.jwt.JwtTokenType;
+import org.sopt.makers.operation.web.admin.dto.request.SignUpRequest;
+import org.sopt.makers.operation.web.admin.dto.response.SignUpResponse;
+import org.sopt.makers.operation.web.admin.dto.request.LoginRequest;
+import org.sopt.makers.operation.web.admin.dto.response.LoginResponse;
+import org.sopt.makers.operation.web.admin.dto.response.RefreshResponse;
+import org.sopt.makers.operation.admin.repository.AdminRepository;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RequiredArgsConstructor
+@Service
+@Transactional(readOnly = true)
+public class AdminServiceImpl implements AdminService {
+
+ private final JwtTokenProvider jwtTokenProvider;
+ private final PasswordEncoder passwordEncoder;
+ private final AdminRepository adminRepository;
+
+ @Override
+ @Transactional
+ public SignUpResponse signUp(SignUpRequest request){
+ checkEmailDuplicated(request.email());
+ val admin = adminRepository.save(request.toEntity());
+ return SignUpResponse.of(admin);
+ }
+
+ private void checkEmailDuplicated(String email) {
+ val isExist = adminRepository.existsByEmail(email);
+ if (isExist) {
+ throw new AdminFailureException(DUPLICATED_EMAIL);
+ }
+ }
+
+ @Override
+ @Transactional
+ public LoginResponse login(LoginRequest request) {
+ val admin = findByEmail(request.email());
+ checkPasswordMatched(request.password(), admin);
+ checkAdminAllowed(admin);
+ val refreshToken = generateRefreshToken(admin);
+ admin.updateRefreshToken(refreshToken);
+ val accessToken = generateAccessToken(admin);
+ return LoginResponse.of(admin, accessToken);
+ }
+
+ private String generateAccessToken(Admin admin) {
+ val adminAuthentication = new AdminAuthentication(admin.getId(), null, null);
+ return jwtTokenProvider.generateAccessToken(adminAuthentication);
+ }
+
+ private String generateRefreshToken(Admin admin) {
+ val authentication = new AdminAuthentication(admin.getId(), null, null);
+ return jwtTokenProvider.generateRefreshToken(authentication);
+ }
+
+ private Admin findByEmail(String email) {
+ return adminRepository.findByEmail(email)
+ .orElseThrow(() -> new AdminFailureException(INVALID_EMAIL));
+ }
+
+ private void checkPasswordMatched(String password, Admin admin) { // TODO: admin 내부로 옮기는 게 좋지 않을까..?
+ if (!passwordEncoder.matches(password, admin.getPassword())) {
+ throw new AdminFailureException(INVALID_PASSWORD);
+ }
+ }
+
+ private void checkAdminAllowed(Admin admin) {
+ if (admin.isNotAllowed()) {
+ throw new AdminFailureException(NOT_APPROVED_ACCOUNT);
+ }
+ }
+
+ @Override
+ @Transactional
+ public RefreshResponse refresh(String refreshToken) {
+ val adminId = jwtTokenProvider.getId(refreshToken, JwtTokenType.REFRESH_TOKEN);
+ val admin = findById(adminId);
+ validateRefreshToken(admin, refreshToken);
+ val newAccessToken = generateAccessToken(admin);
+
+ return RefreshResponse.of(newAccessToken, admin);
+ }
+
+ public void validateRefreshToken(Admin admin, String refreshToken) {
+ if(!admin.getRefreshToken().equals(refreshToken)) {
+ throw new AdminFailureException(INVALID_REFRESH_TOKEN);
+ }
+ }
+
+ private Admin findById(Long adminId) {
+ return adminRepository.findById(adminId)
+ .orElseThrow(() -> new AdminFailureException(INVALID_MEMBER));
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApi.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApi.java
new file mode 100644
index 00000000..b0052754
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApi.java
@@ -0,0 +1,122 @@
+package org.sopt.makers.operation.web.alarm.api;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.alarm.domain.Status;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmCreateRequest;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmSendRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+public interface AlarmApi {
+
+ @Operation(
+ summary = "알림 전송 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "알림 전송 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "알림 전송에 실패하였습니다."
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "전송된 알림입니다."
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "비활동 유저 불러오기에 실패하였습니다."
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "알림이 존재하지 않습니다."
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> sendAlarm(@RequestBody AlarmSendRequest request);
+
+ @Operation(
+ summary = "알림 생성 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "알림 생성 성공"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> createAlarm(@RequestBody AlarmCreateRequest request);
+
+ @Operation(
+ summary = "알림 리스트 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "알림 리스트 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getAlarms(
+ @RequestParam(required = false) Integer generation,
+ @RequestParam(required = false) Part part,
+ @RequestParam(required = false) Status status,
+ Pageable pageable
+ );
+
+ @Operation(
+ summary = "알림 상세 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "알림 상세 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "알림이 존재하지 않습니다."
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getAlarm(@PathVariable long alarmId);
+
+ @Operation(
+ summary = "알림 삭제 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "알림 삭제 성공"
+ ),
+ @ApiResponse(
+ responseCode = "404",
+ description = "알림이 존재하지 않습니다."
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> deleteAlarm(@PathVariable long alarmId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApiController.java
new file mode 100644
index 00000000..88c1523c
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/api/AlarmApiController.java
@@ -0,0 +1,71 @@
+package org.sopt.makers.operation.web.alarm.api;
+
+import static org.sopt.makers.operation.code.success.web.AlarmSuccessCode.*;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.alarm.domain.Status;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmCreateRequest;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmSendRequest;
+import org.sopt.makers.operation.web.alarm.service.AlarmService;
+import org.springframework.data.domain.Pageable;
+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.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/alarms")
+public class AlarmApiController implements AlarmApi {
+
+ private final AlarmService alarmService;
+
+ @Override
+ @PostMapping("/send")
+ public ResponseEntity> sendAlarm(AlarmSendRequest request) {
+ alarmService.sendAlarm(request);
+ return ApiResponseUtil.success(SUCCESS_SEND_ALARM);
+ }
+
+ @Override
+ @PostMapping
+ public ResponseEntity> createAlarm(AlarmCreateRequest request) {
+ val response = alarmService.saveAlarm(request);
+ return ApiResponseUtil.success(SUCCESS_CREATE_ALARM, response.alarmId());
+ }
+
+ @Override
+ @GetMapping
+ public ResponseEntity> getAlarms(
+ @RequestParam(required = false) Integer generation,
+ @RequestParam(required = false) Part part,
+ @RequestParam(required = false) Status status,
+ Pageable pageable
+ ) {
+ val response = alarmService.getAlarms(generation, part, status, pageable);
+ return ApiResponseUtil.success(SUCCESS_GET_ALARMS, response);
+ }
+
+ @Override
+ @GetMapping("/{alarmId}")
+ public ResponseEntity> getAlarm(@PathVariable long alarmId) {
+ val response = alarmService.getAlarm(alarmId);
+ return ApiResponseUtil.success(SUCCESS_GET_ALARM, response);
+ }
+
+ @Override
+ @DeleteMapping("/{alarmId}")
+ public ResponseEntity> deleteAlarm(@PathVariable long alarmId) {
+ alarmService.deleteAlarm(alarmId);
+ return ApiResponseUtil.success(SUCCESS_DELETE_ALARM);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmCreateRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmCreateRequest.java
new file mode 100644
index 00000000..d2ad0347
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmCreateRequest.java
@@ -0,0 +1,34 @@
+package org.sopt.makers.operation.web.alarm.dto.request;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Alarm;
+import org.sopt.makers.operation.alarm.domain.Attribute;
+
+public record AlarmCreateRequest(
+ int generation,
+ int generationAt,
+ Attribute attribute,
+ String title,
+ String content,
+ String link,
+ Boolean isActive,
+ Part part,
+ List targetList
+) {
+
+ public Alarm toEntity() {
+ return Alarm.builder()
+ .generation(this.generation)
+ .generationAt(this.generationAt)
+ .attribute(this.attribute)
+ .title(this.title)
+ .content(this.content)
+ .link(this.link)
+ .isActive(this.isActive)
+ .part(this.part)
+ .targetList(this.targetList)
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmSendRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmSendRequest.java
new file mode 100644
index 00000000..a3a30aea
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/request/AlarmSendRequest.java
@@ -0,0 +1,6 @@
+package org.sopt.makers.operation.web.alarm.dto.request;
+
+public record AlarmSendRequest(
+ long alarmId
+) {
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmCreateResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmCreateResponse.java
new file mode 100644
index 00000000..d6e237c2
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmCreateResponse.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.web.alarm.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.alarm.domain.Alarm;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AlarmCreateResponse(
+ long alarmId
+) {
+
+ public static AlarmCreateResponse of(Alarm alarm) {
+ return AlarmCreateResponse.builder()
+ .alarmId(alarm.getId())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmGetResponse.java
new file mode 100644
index 00000000..56fe9ec2
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmGetResponse.java
@@ -0,0 +1,50 @@
+package org.sopt.makers.operation.web.alarm.dto.response;
+
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.*;
+import static java.util.Objects.*;
+import static lombok.AccessLevel.*;
+
+import java.time.LocalDateTime;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Alarm;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AlarmGetResponse(
+ String attribute,
+ @JsonInclude(value = NON_NULL)
+ String part,
+ Boolean isActive,
+ String title,
+ String content,
+ String link,
+ String createdAt,
+ @JsonInclude(value = NON_NULL)
+ String sendAt
+) {
+
+ public static AlarmGetResponse of(Alarm alarm) {
+ return AlarmGetResponse.builder()
+ .attribute(alarm.getAttribute().getName())
+ .part(getPartName(alarm.getPart()))
+ .isActive(alarm.getIsActive())
+ .title(alarm.getTitle())
+ .content(alarm.getContent())
+ .link(alarm.getLink())
+ .createdAt(alarm.getCreatedDate().toString())
+ .sendAt(getSendAt(alarm.getSendAt()))
+ .build();
+ }
+
+ private static String getPartName(Part part) {
+ return nonNull(part) ? part.getName() : null;
+ }
+
+ private static String getSendAt(LocalDateTime sendAt) {
+ return nonNull(sendAt) ? sendAt.toString() : null;
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmListGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmListGetResponse.java
new file mode 100644
index 00000000..c37b31e8
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/dto/response/AlarmListGetResponse.java
@@ -0,0 +1,63 @@
+package org.sopt.makers.operation.web.alarm.dto.response;
+
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.*;
+import static java.util.Objects.*;
+import static lombok.AccessLevel.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Alarm;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AlarmListGetResponse(
+ List alarms,
+ int totalCount
+) {
+
+ public static AlarmListGetResponse of(List alarmList, int totalCount) {
+ return AlarmListGetResponse.builder()
+ .alarms(alarmList.stream().map(AlarmResponse::of).toList())
+ .totalCount(totalCount)
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ private record AlarmResponse(
+ long alarmId,
+ @JsonInclude(value = NON_NULL)
+ String part,
+ String attribute,
+ String title,
+ String content,
+ @JsonInclude(value = NON_NULL)
+ String sendAt,
+ String status
+ ) {
+
+ private static AlarmResponse of(Alarm alarm) {
+ return AlarmResponse.builder()
+ .alarmId(alarm.getId())
+ .part(getPartName(alarm.getPart()))
+ .attribute(alarm.getAttribute().getName())
+ .title(alarm.getTitle())
+ .content(alarm.getContent())
+ .sendAt(getSendAt(alarm.getSendAt()))
+ .status(alarm.getStatus().getName())
+ .build();
+ }
+
+ private static String getPartName(Part part) {
+ return nonNull(part) ? part.getName() : null;
+ }
+
+ private static String getSendAt(LocalDateTime sendAt) {
+ return nonNull(sendAt) ? sendAt.toString() : null;
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmService.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmService.java
new file mode 100644
index 00000000..94a551a6
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmService.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.web.alarm.service;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Status;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmCreateRequest;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmSendRequest;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmCreateResponse;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmGetResponse;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmListGetResponse;
+import org.springframework.data.domain.Pageable;
+
+public interface AlarmService {
+ void sendAlarm(AlarmSendRequest request);
+ AlarmCreateResponse saveAlarm(AlarmCreateRequest requestDTO);
+ AlarmListGetResponse getAlarms(Integer generation, Part part, Status status, Pageable pageable);
+ AlarmGetResponse getAlarm(long alarmId);
+ void deleteAlarm(long alarmId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmServiceImpl.java
new file mode 100644
index 00000000..a6229e65
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/alarm/service/AlarmServiceImpl.java
@@ -0,0 +1,125 @@
+package org.sopt.makers.operation.web.alarm.service;
+
+import static org.sopt.makers.operation.code.failure.AlarmFailureCode.*;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.sopt.makers.operation.client.alarm.AlarmSender;
+import org.sopt.makers.operation.client.alarm.dto.AlarmSenderRequest;
+import org.sopt.makers.operation.client.playground.PlayGroundServer;
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Alarm;
+import org.sopt.makers.operation.alarm.domain.Status;
+import org.sopt.makers.operation.alarm.repository.AlarmRepository;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmCreateRequest;
+import org.sopt.makers.operation.exception.AlarmException;
+import org.sopt.makers.operation.web.alarm.dto.request.AlarmSendRequest;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmCreateResponse;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmGetResponse;
+import org.sopt.makers.operation.web.alarm.dto.response.AlarmListGetResponse;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+public class AlarmServiceImpl implements AlarmService {
+
+ private final AlarmRepository alarmRepository;
+ private final MemberRepository memberRepository;
+
+ private final AlarmSender alarmSender;
+ private final PlayGroundServer playGroundServer;
+ private final ValueConfig valueConfig;
+
+ @Override
+ @Transactional
+ public void sendAlarm(AlarmSendRequest request) {
+ val alarm = getAlarmReadyToSend(request.alarmId());
+ val targets = getTargets(alarm);
+ alarmSender.send(AlarmSenderRequest.of(alarm, targets));
+ alarm.updateToSent();
+ }
+
+ @Override
+ @Transactional
+ public AlarmCreateResponse saveAlarm(AlarmCreateRequest request) {
+ val savedAlarm = alarmRepository.save(request.toEntity());
+ return AlarmCreateResponse.of(savedAlarm);
+ }
+
+ @Override
+ public AlarmListGetResponse getAlarms(Integer generation, Part part, Status status, Pageable pageable) {
+ val alarms = alarmRepository.findOrderByCreatedDate(generation, part, status, pageable);
+ val totalCount = alarmRepository.count(generation, part, status);
+ return AlarmListGetResponse.of(alarms, totalCount);
+ }
+
+ @Override
+ public AlarmGetResponse getAlarm(long alarmId) {
+ val alarm = findAlarm(alarmId);
+ return AlarmGetResponse.of(alarm);
+ }
+
+ @Override
+ @Transactional
+ public void deleteAlarm(long alarmId) {
+ val alarm = findAlarm(alarmId);
+ alarmRepository.delete(alarm);
+ }
+
+ private Alarm getAlarmReadyToSend(long alarmId) {
+ val alarm = findAlarm(alarmId);
+ if (alarm.isSent()) {
+ throw new AlarmException(SENT_ALARM);
+ }
+ return alarm;
+ }
+
+ private Alarm findAlarm(long id) {
+ return alarmRepository.findById(id)
+ .orElseThrow(() -> new AlarmException(INVALID_ALARM));
+ }
+
+ private List getTargets(Alarm alarm) {
+ return alarm.hasTargets()
+ ? alarm.getTargetList()
+ : getTargetsByActivityAndPart(alarm.getIsActive(), alarm.getPart());
+ }
+
+ private List getTargetsByActivityAndPart(boolean isActive, Part part) {
+ return isActive ? getActiveTargets(part) : getInactiveTargets(part);
+ }
+
+ private List getActiveTargets(Part part) {
+ val generation = valueConfig.getGENERATION();
+ val members = memberRepository.find(generation, part);
+ return members.stream()
+ .map(Member::getPlaygroundId)
+ .filter(Objects::nonNull)
+ .map(String::valueOf)
+ .toList();
+ }
+
+ private List getInactiveTargets(Part part) {
+ val generation = valueConfig.getGENERATION();
+ val activePlaygroundIds = getActiveTargets(part);
+ return getPlaygroundIds(generation, part).stream()
+ .filter(id -> !activePlaygroundIds.contains(id))
+ .toList();
+ }
+
+ private List getPlaygroundIds(int generation, Part part) {
+ val members = playGroundServer.getMembers(generation, part);
+ return members.memberIds().stream()
+ .map(String::valueOf)
+ .toList();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApi.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApi.java
new file mode 100644
index 00000000..1efb6170
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApi.java
@@ -0,0 +1,95 @@
+package org.sopt.makers.operation.web.attendnace.api;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.web.attendnace.dto.request.SubAttendanceUpdateRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+public interface WebAttendanceApi {
+
+ @Operation(
+ summary = "출석 상태 변경 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 상태 변경 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> updateSubAttendance(@RequestBody SubAttendanceUpdateRequest request);
+
+ @Operation(
+ summary = "회원별 출석 정보 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "회원별 출석 정보 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> findAttendancesByMember(@PathVariable long memberId);
+
+ @Operation(
+ summary = "출석 점수 갱신 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 점수 갱신 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> updateMemberScore(@PathVariable long memberId);
+
+ @Operation(
+ summary = "세션별 출석 정보 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션별 출석 정보 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> findAttendancesByLecture(
+ @PathVariable long lectureId,
+ @RequestParam(required = false) Part part,
+ Pageable pageable);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApiController.java
new file mode 100644
index 00000000..062b682d
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/api/WebAttendanceApiController.java
@@ -0,0 +1,61 @@
+package org.sopt.makers.operation.web.attendnace.api;
+
+import static org.sopt.makers.operation.code.success.web.AttendanceSuccessCode.*;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.web.attendnace.dto.request.SubAttendanceUpdateRequest;
+import org.sopt.makers.operation.web.attendnace.service.WebAttendanceService;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/attendances")
+public class WebAttendanceApiController implements WebAttendanceApi {
+
+ private final WebAttendanceService attendanceService;
+
+ @Override
+ @PatchMapping
+ public ResponseEntity> updateSubAttendance(@RequestBody SubAttendanceUpdateRequest request) {
+ val response = attendanceService.updateSubAttendance(request);
+ return ApiResponseUtil.success(SUCCESS_UPDATE_ATTENDANCE_STATUS, response);
+ }
+
+ @Override
+ @GetMapping("/{memberId}")
+ public ResponseEntity> findAttendancesByMember(@PathVariable long memberId) {
+ val response = attendanceService.getAttendancesByMember(memberId);
+ return ApiResponseUtil.success(SUCCESS_GET_MEMBER_ATTENDANCE, response);
+ }
+
+ @Override
+ @PatchMapping("/member/{memberId}")
+ public ResponseEntity> updateMemberScore(@PathVariable long memberId) {
+ val response = attendanceService.updateMemberAllScore(memberId);
+ return ApiResponseUtil.success(SUCCESS_UPDATE_MEMBER_SCORE, response.score());
+ }
+
+ @Override
+ @GetMapping("/lecture/{lectureId}")
+ public ResponseEntity> findAttendancesByLecture(
+ @PathVariable long lectureId,
+ @RequestParam(required = false) Part part,
+ Pageable pageable
+ ) {
+ val response = attendanceService.getAttendancesByLecture(lectureId, part, pageable);
+ return ApiResponseUtil.success(SUCCESS_GET_ATTENDANCES, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/request/SubAttendanceUpdateRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/request/SubAttendanceUpdateRequest.java
new file mode 100644
index 00000000..91b22a61
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/request/SubAttendanceUpdateRequest.java
@@ -0,0 +1,9 @@
+package org.sopt.makers.operation.web.attendnace.dto.request;
+
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+
+public record SubAttendanceUpdateRequest(
+ long subAttendanceId,
+ AttendanceStatus status
+) {
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByLectureGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByLectureGetResponse.java
new file mode 100644
index 00000000..9e048d9f
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByLectureGetResponse.java
@@ -0,0 +1,77 @@
+package org.sopt.makers.operation.web.attendnace.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.member.domain.Member;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AttendanceListByLectureGetResponse(
+ List attendances,
+ int totalCount
+) {
+
+ public static AttendanceListByLectureGetResponse of(List attendanceList, int totalCount) {
+ return AttendanceListByLectureGetResponse.builder()
+ .attendances(attendanceList.stream().map(AttendanceResponse::of).toList())
+ .totalCount(totalCount)
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ private record AttendanceResponse(
+ long attendanceId,
+ MemberResponse member,
+ List attendances,
+ float updatedScore
+ ) {
+ private static AttendanceResponse of(Attendance attendance) {
+ return AttendanceResponse.builder()
+ .attendanceId(attendance.getId())
+ .member(MemberResponse.of(attendance.getMember()))
+ .attendances(attendance.getSubAttendances().stream().map(SubAttendanceVO::of).toList())
+ .updatedScore(attendance.getScore())
+ .build();
+ }
+ }
+
+ @Builder(access = PRIVATE)
+ private record MemberResponse(
+ long memberId,
+ String name,
+ String university,
+ String part
+ ) {
+ private static MemberResponse of(Member member) {
+ return MemberResponse.builder()
+ .memberId(member.getId())
+ .name(member.getName())
+ .university(member.getUniversity())
+ .part(member.getPart().getName())
+ .build();
+ }
+ }
+
+ @Builder(access = PRIVATE)
+ private record SubAttendanceVO(
+ long subAttendanceId,
+ int round,
+ AttendanceStatus status,
+ String updateAt
+ ) {
+ private static SubAttendanceVO of(SubAttendance subAttendance) {
+ return SubAttendanceVO.builder()
+ .subAttendanceId(subAttendance.getId())
+ .round(subAttendance.getSubLecture().getRound())
+ .status(subAttendance.getStatus())
+ .updateAt(subAttendance.getLastModifiedDate().toString())
+ .build();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByMemberGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByMemberGetResponse.java
new file mode 100644
index 00000000..7a6a13fc
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/AttendanceListByMemberGetResponse.java
@@ -0,0 +1,67 @@
+package org.sopt.makers.operation.web.attendnace.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.member.domain.Member;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AttendanceListByMemberGetResponse(
+ String name,
+ float score,
+ String part,
+ String university,
+ String phone,
+ List lectures
+) {
+
+ public static AttendanceListByMemberGetResponse of(Member member, List attendances) {
+ return AttendanceListByMemberGetResponse.builder()
+ .name(member.getName())
+ .score(member.getScore())
+ .part(member.getPart().getName())
+ .university(member.getUniversity())
+ .phone(member.getPhone())
+ .lectures(attendances.stream().map(LectureResponse::of).toList())
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ record LectureResponse(
+ String lecture,
+ float additiveScore,
+ String status,
+ List attendances
+ ) {
+
+ public static LectureResponse of(Attendance attendance) {
+ return LectureResponse.builder()
+ .lecture(attendance.getLecture().getName())
+ .additiveScore(attendance.getScore())
+ .status(attendance.getStatus().getName())
+ .attendances(attendance.getSubAttendances().stream().map(AttendanceResponse::of).toList())
+ .build();
+ }
+ }
+
+ @Builder(access = PRIVATE)
+ record AttendanceResponse(
+ int round,
+ String status,
+ String date
+ ) {
+
+ public static AttendanceResponse of(SubAttendance subAttendance) {
+ return AttendanceResponse.builder()
+ .round(subAttendance.getSubLecture().getRound())
+ .status(subAttendance.getStatus().getName())
+ .date(subAttendance.getLastModifiedDate().toString())
+ .build();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/MemberScoreUpdateResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/MemberScoreUpdateResponse.java
new file mode 100644
index 00000000..9069918a
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/MemberScoreUpdateResponse.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.web.attendnace.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.member.domain.Member;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record MemberScoreUpdateResponse(
+ float score
+) {
+
+ public static MemberScoreUpdateResponse of(Member member) {
+ return MemberScoreUpdateResponse.builder()
+ .score(member.getScore())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/SubAttendanceUpdateResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/SubAttendanceUpdateResponse.java
new file mode 100644
index 00000000..15f6faba
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/dto/response/SubAttendanceUpdateResponse.java
@@ -0,0 +1,22 @@
+package org.sopt.makers.operation.web.attendnace.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record SubAttendanceUpdateResponse(
+ long subAttendanceId,
+ AttendanceStatus status
+) {
+
+ public static SubAttendanceUpdateResponse of(SubAttendance subAttendance) {
+ return SubAttendanceUpdateResponse.builder()
+ .subAttendanceId(subAttendance.getId())
+ .status(subAttendance.getStatus())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceService.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceService.java
new file mode 100644
index 00000000..affade26
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceService.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.web.attendnace.service;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.web.attendnace.dto.request.SubAttendanceUpdateRequest;
+import org.sopt.makers.operation.web.attendnace.dto.response.AttendanceListByLectureGetResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.AttendanceListByMemberGetResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.MemberScoreUpdateResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.SubAttendanceUpdateResponse;
+import org.springframework.data.domain.Pageable;
+
+public interface WebAttendanceService {
+ SubAttendanceUpdateResponse updateSubAttendance(SubAttendanceUpdateRequest request);
+ AttendanceListByMemberGetResponse getAttendancesByMember(long memberId);
+ MemberScoreUpdateResponse updateMemberAllScore(long memberId);
+ AttendanceListByLectureGetResponse getAttendancesByLecture(long lectureId, Part part, Pageable pageable);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceServiceImpl.java
new file mode 100644
index 00000000..b64ebca2
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/attendnace/service/WebAttendanceServiceImpl.java
@@ -0,0 +1,86 @@
+package org.sopt.makers.operation.web.attendnace.service;
+
+import static org.sopt.makers.operation.code.failure.AttendanceFailureCode.*;
+import static org.sopt.makers.operation.code.failure.LectureFailureCode.*;
+import static org.sopt.makers.operation.code.failure.MemberFailureCode.*;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.attendance.repository.attendance.AttendanceRepository;
+import org.sopt.makers.operation.attendance.repository.subAttendance.SubAttendanceRepository;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.repository.lecture.LectureRepository;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.sopt.makers.operation.exception.LectureException;
+import org.sopt.makers.operation.exception.MemberException;
+import org.sopt.makers.operation.web.attendnace.dto.request.SubAttendanceUpdateRequest;
+import org.sopt.makers.operation.web.attendnace.dto.response.AttendanceListByMemberGetResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.AttendanceListByLectureGetResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.MemberScoreUpdateResponse;
+import org.sopt.makers.operation.web.attendnace.dto.response.SubAttendanceUpdateResponse;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class WebAttendanceServiceImpl implements WebAttendanceService {
+
+ private final AttendanceRepository attendanceRepository;
+ private final SubAttendanceRepository subAttendanceRepository;
+ private final MemberRepository memberRepository;
+ private final LectureRepository lectureRepository;
+
+ @Override
+ @Transactional
+ public SubAttendanceUpdateResponse updateSubAttendance(SubAttendanceUpdateRequest request) {
+ val subAttendance = findSubAttendance(request.subAttendanceId());
+ subAttendance.updateStatus(request.status());
+ return SubAttendanceUpdateResponse.of(subAttendance);
+ }
+
+ @Override
+ public AttendanceListByMemberGetResponse getAttendancesByMember(long memberId) {
+ val member = findMember(memberId);
+ val attendances = attendanceRepository.findFetchJoin(member);
+ return AttendanceListByMemberGetResponse.of(member, attendances);
+ }
+
+ @Override
+ @Transactional
+ public MemberScoreUpdateResponse updateMemberAllScore(long memberId) {
+ val member = findMember(memberId);
+ member.updateTotalScore();
+ return MemberScoreUpdateResponse.of(member);
+ }
+
+ @Override
+ public AttendanceListByLectureGetResponse getAttendancesByLecture(long lectureId, Part part, Pageable pageable) {
+ val lecture = findLecture(lectureId);
+ val attendances = attendanceRepository.findFetchJoin(lecture, part, pageable);
+ val totalCount = attendanceRepository.count(lecture, part);
+ return AttendanceListByLectureGetResponse.of(attendances, totalCount);
+ }
+
+ private SubAttendance findSubAttendance(long id) {
+ return subAttendanceRepository.findById(id)
+ .orElseThrow(() -> new LectureException(INVALID_ATTENDANCE));
+ }
+
+ private Member findMember(long id) {
+ return memberRepository.findById(id)
+ .orElseThrow(() -> new MemberException(INVALID_MEMBER));
+ }
+
+ private Lecture findLecture(long id) {
+ return lectureRepository.findById(id)
+ .orElseThrow(() -> new LectureException(INVALID_LECTURE));
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApi.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApi.java
new file mode 100644
index 00000000..802ff563
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApi.java
@@ -0,0 +1,151 @@
+package org.sopt.makers.operation.web.lecture.api;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.web.lecture.dto.request.SubLectureStartRequest;
+import org.sopt.makers.operation.web.lecture.dto.request.LectureCreateRequest;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+public interface WebLectureApi {
+
+ @Operation(
+ summary = "세션 생성 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 생성 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> createLecture(@RequestBody LectureCreateRequest request);
+
+ @Operation(
+ summary = "세션 리스트 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 리스트 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getLectures(
+ @RequestParam int generation,
+ @RequestParam(required = false) Part part);
+
+ @Operation(
+ summary = "세션 단일 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 단일 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getLecture(@PathVariable long lectureId);
+
+ @Operation(
+ summary = "출석 시작 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "출석 시작 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> startSubLecture(@RequestBody SubLectureStartRequest request);
+
+ @Operation(
+ summary = "세션 종료 후 출석 점수 갱신 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 종료 후 출석 점수 갱신 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> endLecture(@PathVariable long lectureId);
+
+ @Operation(
+ summary = "세션 삭제 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 삭제 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> deleteLecture(@PathVariable long lectureId);
+
+ @Operation(
+ summary = "세션 팝업용 상세 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "세션 팝업용 상세 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getLectureDetail(@PathVariable long lectureId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApiController.java
new file mode 100644
index 00000000..33913f1e
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/api/WebLectureApiController.java
@@ -0,0 +1,83 @@
+package org.sopt.makers.operation.web.lecture.api;
+
+import static org.sopt.makers.operation.code.success.web.LectureSuccessCode.*;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.web.lecture.dto.request.SubLectureStartRequest;
+import org.sopt.makers.operation.web.lecture.dto.request.LectureCreateRequest;
+import org.sopt.makers.operation.web.lecture.service.WebLectureService;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/lectures")
+public class WebLectureApiController implements WebLectureApi {
+
+ private final WebLectureService lectureService;
+
+ @Override
+ @PostMapping
+ public ResponseEntity> createLecture(@RequestBody LectureCreateRequest request) {
+ val response = lectureService.createLecture(request);
+ return ApiResponseUtil.success(SUCCESS_CREATE_LECTURE, response.lectureId());
+ }
+
+ @Override
+ @GetMapping
+ public ResponseEntity> getLectures(
+ @RequestParam int generation,
+ @RequestParam(required = false) Part part
+ ) {
+ val response = lectureService.getLectures(generation, part);
+ return ApiResponseUtil.success(SUCCESS_GET_LECTURES, response);
+ }
+
+ @Override
+ @GetMapping("/{lectureId}")
+ public ResponseEntity> getLecture(@PathVariable long lectureId) {
+ val response = lectureService.getLecture(lectureId);
+ return ApiResponseUtil.success(SUCCESS_GET_LECTURE, response);
+ }
+
+ @Override
+ @PatchMapping("/attendance")
+ public ResponseEntity> startSubLecture(SubLectureStartRequest request) {
+ val response = lectureService.startSubLecture(request);
+ return ApiResponseUtil.success(SUCCESS_START_ATTENDANCE, response);
+ }
+
+ @Override
+ @PatchMapping("/{lectureId}")
+ public ResponseEntity> endLecture(@PathVariable long lectureId) {
+ lectureService.endLecture(lectureId);
+ return ApiResponseUtil.success(SUCCESS_END_LECTURE);
+ }
+
+ @Override
+ @DeleteMapping("/{lectureId}")
+ public ResponseEntity> deleteLecture(@PathVariable long lectureId) {
+ lectureService.deleteLecture(lectureId);
+ return ApiResponseUtil.success(SUCCESS_DELETE_LECTURE);
+ }
+
+ @Override
+ @GetMapping("/detail/{lectureId}")
+ public ResponseEntity> getLectureDetail(@PathVariable long lectureId) {
+ val response = lectureService.getLectureDetail(lectureId);
+ return ApiResponseUtil.success(SUCCESS_GET_LECTURE, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/LectureCreateRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/LectureCreateRequest.java
new file mode 100644
index 00000000..17c6f32c
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/LectureCreateRequest.java
@@ -0,0 +1,47 @@
+package org.sopt.makers.operation.web.lecture.dto.request;
+
+import static lombok.AccessLevel.*;
+import static org.sopt.makers.operation.code.failure.LectureFailureCode.*;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.exception.DateTimeParseCustomException;
+
+import lombok.Builder;
+import lombok.NonNull;
+
+@Builder(access = PRIVATE)
+public record LectureCreateRequest(
+ @NonNull Part part,
+ @NonNull String name,
+ int generation,
+ String place,
+ String startDate,
+ String endDate,
+ @NonNull Attribute attribute
+) {
+
+ public Lecture toEntity() {
+ return Lecture.builder()
+ .name(this.name)
+ .part(this.part)
+ .generation(this.generation)
+ .place(this.place)
+ .startDate(convertLocalDateTime(this.startDate))
+ .endDate(convertLocalDateTime(this.endDate))
+ .attribute(this.attribute)
+ .build();
+ }
+
+ private LocalDateTime convertLocalDateTime(String date) {
+ try {
+ return LocalDateTime.parse(date);
+ } catch (DateTimeParseException exception) {
+ throw new DateTimeParseCustomException(INVALID_DATE_PATTERN, date, exception.getErrorIndex());
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/SubLectureStartRequest.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/SubLectureStartRequest.java
new file mode 100644
index 00000000..aedeec2c
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/request/SubLectureStartRequest.java
@@ -0,0 +1,10 @@
+package org.sopt.makers.operation.web.lecture.dto.request;
+
+import lombok.NonNull;
+
+public record SubLectureStartRequest(
+ long lectureId,
+ int round,
+ @NonNull String code
+) {
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/AttendanceStatusListResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/AttendanceStatusListResponse.java
new file mode 100644
index 00000000..bddfb4bb
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/AttendanceStatusListResponse.java
@@ -0,0 +1,41 @@
+package org.sopt.makers.operation.web.lecture.dto.response;
+
+import static lombok.AccessLevel.*;
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record AttendanceStatusListResponse(
+ int attendance,
+ int absent,
+ int tardy,
+ int unknown
+) {
+
+ public static AttendanceStatusListResponse of(Lecture lecture) {
+ return AttendanceStatusListResponse.builder()
+ .attendance(getCount(lecture, ATTENDANCE))
+ .absent(getAbsentCount(lecture))
+ .tardy(getCount(lecture, TARDY))
+ .unknown(getUnknownCount(lecture))
+ .build();
+ }
+
+ private static int getCount(Lecture lecture, AttendanceStatus status) {
+ return (int)lecture.getAttendances().stream()
+ .filter(attendance -> attendance.getStatus().equals(status))
+ .count();
+ }
+
+ private static int getAbsentCount(Lecture lecture) {
+ return lecture.isEnd() ? getCount(lecture, ABSENT) : 0;
+ }
+
+ private static int getUnknownCount(Lecture lecture) {
+ return lecture.isEnd() ? 0 : getCount(lecture, ABSENT);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureCreateResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureCreateResponse.java
new file mode 100644
index 00000000..c47b36f0
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureCreateResponse.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.web.lecture.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.lecture.domain.Lecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record LectureCreateResponse(
+ long lectureId
+) {
+
+ public static LectureCreateResponse of(Lecture lecture) {
+ return LectureCreateResponse.builder()
+ .lectureId(lecture.getId())
+ .build();
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureDetailResponseDTO.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureDetailGetResponse.java
similarity index 57%
rename from src/main/java/org/sopt/makers/operation/dto/lecture/LectureDetailResponseDTO.java
rename to operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureDetailGetResponse.java
index 95042350..e09424c0 100644
--- a/src/main/java/org/sopt/makers/operation/dto/lecture/LectureDetailResponseDTO.java
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureDetailGetResponse.java
@@ -1,12 +1,14 @@
-package org.sopt.makers.operation.dto.lecture;
+package org.sopt.makers.operation.web.lecture.dto.response;
-import org.sopt.makers.operation.entity.lecture.Lecture;
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.lecture.domain.Lecture;
import lombok.Builder;
-@Builder
-public record LectureDetailResponseDTO(
- Long lectureId,
+@Builder(access = PRIVATE)
+public record LectureDetailGetResponse(
+ long lectureId,
String part,
String name,
String place,
@@ -15,8 +17,9 @@ public record LectureDetailResponseDTO(
String endDate,
int generation
) {
- public static LectureDetailResponseDTO of(Lecture lecture) {
- return LectureDetailResponseDTO.builder()
+
+ public static LectureDetailGetResponse of(Lecture lecture) {
+ return LectureDetailGetResponse.builder()
.lectureId(lecture.getId())
.part(lecture.getPart().getName())
.name(lecture.getName())
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureGetResponse.java
new file mode 100644
index 00000000..c478c31a
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureGetResponse.java
@@ -0,0 +1,64 @@
+package org.sopt.makers.operation.web.lecture.dto.response;
+
+import static java.util.Objects.*;
+import static lombok.AccessLevel.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.LectureStatus;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record LectureGetResponse(
+ long lectureId,
+ String name,
+ int generation,
+ Part part,
+ Attribute attribute,
+ List subLectures,
+ AttendanceStatusListResponse attendances,
+ LectureStatus status
+
+) {
+
+ public static LectureGetResponse of(Lecture lecture) {
+ return LectureGetResponse.builder()
+ .lectureId(lecture.getId())
+ .name(lecture.getName())
+ .generation(lecture.getGeneration())
+ .part(lecture.getPart())
+ .attribute(lecture.getAttribute())
+ .subLectures(lecture.getSubLectures().stream().map(SubLectureResponse::of).toList())
+ .attendances(AttendanceStatusListResponse.of(lecture))
+ .status(lecture.getLectureStatus())
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ public record SubLectureResponse(
+ long subLectureId,
+ int round,
+ String startAt,
+ String code
+ ) {
+
+ private static SubLectureResponse of(SubLecture subLecture) {
+ return SubLectureResponse.builder()
+ .subLectureId(subLecture.getId())
+ .round(subLecture.getRound())
+ .startAt(getStartAt(subLecture.getStartAt()))
+ .code(subLecture.getCode())
+ .build();
+ }
+
+ private static String getStartAt(LocalDateTime startAt) {
+ return nonNull(startAt) ? startAt.toString() : null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureListGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureListGetResponse.java
new file mode 100644
index 00000000..f1ef0c0f
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/LectureListGetResponse.java
@@ -0,0 +1,55 @@
+package org.sopt.makers.operation.web.lecture.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record LectureListGetResponse(
+ int generation,
+ List lectures
+) {
+
+ public static LectureListGetResponse of(int generation, List lectureList) {
+ return LectureListGetResponse.builder()
+ .generation(generation)
+ .lectures(lectureList.stream().map(LectureResponse::of).toList())
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ public record LectureResponse(
+ long lectureId,
+ String name,
+ Part partValue,
+ String partName,
+ String startDate,
+ String endDate,
+ Attribute attributeValue,
+ String attributeName,
+ String place,
+ AttendanceStatusListResponse attendances
+ ) {
+
+ private static LectureResponse of(Lecture lecture) {
+ return LectureResponse.builder()
+ .lectureId(lecture.getId())
+ .name(lecture.getName())
+ .partValue(lecture.getPart())
+ .partName(lecture.getPart().getName())
+ .startDate(lecture.getStartDate().toString())
+ .endDate(lecture.getEndDate().toString())
+ .attributeValue(lecture.getAttribute())
+ .attributeName(lecture.getAttribute().getName())
+ .place(lecture.getPlace())
+ .attendances(AttendanceStatusListResponse.of(lecture))
+ .build();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/SubLectureStartResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/SubLectureStartResponse.java
new file mode 100644
index 00000000..1e6941b8
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/dto/response/SubLectureStartResponse.java
@@ -0,0 +1,22 @@
+package org.sopt.makers.operation.web.lecture.dto.response;
+
+import static lombok.AccessLevel.*;
+
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record SubLectureStartResponse(
+ long lectureId,
+ long subLectureId
+) {
+
+ public static SubLectureStartResponse of(Lecture lecture, SubLecture subLecture) {
+ return SubLectureStartResponse.builder()
+ .lectureId(lecture.getId())
+ .subLectureId(subLecture.getId())
+ .build();
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureService.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureService.java
new file mode 100644
index 00000000..8fa12dcb
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureService.java
@@ -0,0 +1,21 @@
+package org.sopt.makers.operation.web.lecture.service;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.web.lecture.dto.request.SubLectureStartRequest;
+import org.sopt.makers.operation.web.lecture.dto.request.LectureCreateRequest;
+import org.sopt.makers.operation.web.lecture.dto.response.SubLectureStartResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureCreateResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureDetailGetResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureGetResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureListGetResponse;
+
+public interface WebLectureService {
+ LectureCreateResponse createLecture(LectureCreateRequest request);
+ LectureListGetResponse getLectures(int generation, Part part);
+ LectureGetResponse getLecture(long lectureId);
+ SubLectureStartResponse startSubLecture(SubLectureStartRequest request);
+ void endLecture(long lectureId);
+ void endLectures();
+ void deleteLecture(long lectureId);
+ LectureDetailGetResponse getLectureDetail(long lectureId);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureServiceImpl.java
new file mode 100644
index 00000000..c11adc30
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/lecture/service/WebLectureServiceImpl.java
@@ -0,0 +1,199 @@
+package org.sopt.makers.operation.web.lecture.service;
+
+import static org.sopt.makers.operation.code.failure.LectureFailureCode.*;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.sopt.makers.operation.client.alarm.AlarmSender;
+import org.sopt.makers.operation.client.alarm.dto.AlarmSenderRequest;
+import org.sopt.makers.operation.config.ValueConfig;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.attendance.repository.attendance.AttendanceRepository;
+import org.sopt.makers.operation.attendance.repository.subAttendance.SubAttendanceRepository;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+import org.sopt.makers.operation.lecture.repository.lecture.LectureRepository;
+import org.sopt.makers.operation.lecture.repository.subLecture.SubLectureRepository;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.sopt.makers.operation.exception.LectureException;
+import org.sopt.makers.operation.exception.SubLectureException;
+import org.sopt.makers.operation.web.lecture.dto.request.SubLectureStartRequest;
+import org.sopt.makers.operation.web.lecture.dto.request.LectureCreateRequest;
+import org.sopt.makers.operation.web.lecture.dto.response.SubLectureStartResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureCreateResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureDetailGetResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureGetResponse;
+import org.sopt.makers.operation.web.lecture.dto.response.LectureListGetResponse;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class WebLectureServiceImpl implements WebLectureService {
+
+ private final LectureRepository lectureRepository;
+ private final SubLectureRepository subLectureRepository;
+ private final AttendanceRepository attendanceRepository;
+ private final SubAttendanceRepository subAttendanceRepository;
+ private final MemberRepository memberRepository;
+
+ private final AlarmSender alarmSender;
+ private final ValueConfig valueConfig;
+
+ @Override
+ @Transactional
+ public LectureCreateResponse createLecture(LectureCreateRequest request) {
+ val savedLecture = saveLecture(request);
+ createAttendances(request.generation(), request.part(), savedLecture);
+ return LectureCreateResponse.of(savedLecture);
+ }
+
+ @Override
+ public LectureListGetResponse getLectures(int generation, Part part) {
+ val lectures = lectureRepository.find(generation, part);
+ return LectureListGetResponse.of(generation, lectures);
+ }
+
+ @Override
+ public LectureGetResponse getLecture(long lectureId) {
+ val lecture = findLecture(lectureId);
+ return LectureGetResponse.of(lecture);
+ }
+
+ @Override
+ @Transactional
+ public SubLectureStartResponse startSubLecture(SubLectureStartRequest request) {
+ val lecture = getLectureToStartAttendance(request.lectureId(), request.round());
+ val subLecture = getSubLectureToStartAttendance(lecture, request.round());
+ subLecture.updateCode(request.code());
+ return SubLectureStartResponse.of(lecture, subLecture);
+ }
+
+ @Override
+ @Transactional
+ public void endLecture(long lectureId) {
+ val lecture = getLectureReadyToEnd(lectureId);
+ lecture.updateToEnd();
+ sendAlarm(lecture);
+ }
+
+ @Override
+ @Transactional
+ public void endLectures() {
+ val lectures = lectureRepository.findLecturesReadyToEnd();
+ lectures.forEach(lecture -> endLecture(lecture.getId()));
+ }
+
+ @Override
+ @Transactional
+ public void deleteLecture(long lectureId) {
+ val lecture = getLectureToDelete(lectureId);
+ deleteRelationship(lecture);
+ lectureRepository.deleteById(lectureId);
+ }
+
+ @Override
+ public LectureDetailGetResponse getLectureDetail(long lectureId) {
+ val lecture = findLecture(lectureId);
+ return LectureDetailGetResponse.of(lecture);
+ }
+
+ private Lecture saveLecture(LectureCreateRequest request) {
+ val savedLecture = lectureRepository.save(request.toEntity());
+ createSubLectures(savedLecture);
+ return savedLecture;
+ }
+
+ private void createSubLectures(Lecture lecture) {
+ val maxRound = valueConfig.getSUB_LECTURE_MAX_ROUND();
+ Stream.iterate(1, i -> i + 1).limit(maxRound)
+ .forEach(round -> saveSubLecture(lecture, round));
+ }
+
+ private void saveSubLecture(Lecture lecture, int round) {
+ subLectureRepository.save(new SubLecture(lecture, round));
+ }
+
+ private void createAttendances(int generation, Part part, Lecture lecture) {
+ val members = memberRepository.find(generation, part);
+ members.forEach(member -> saveAttendance(member, lecture));
+ }
+
+ private void saveAttendance(Member member, Lecture lecture) {
+ val savedAttendance = attendanceRepository.save(new Attendance(member, lecture));
+ createSubAttendances(savedAttendance);
+ }
+
+ private void createSubAttendances(Attendance attendance) {
+ val subLectures = attendance.getLecture().getSubLectures();
+ subLectures.forEach(subLecture -> saveSubAttendance(attendance, subLecture));
+ }
+
+ private void saveSubAttendance(Attendance attendance, SubLecture subLecture) {
+ subAttendanceRepository.save(new SubAttendance(attendance, subLecture));
+ }
+
+ private Lecture findLecture(long id) {
+ return lectureRepository.findById(id)
+ .orElseThrow(() -> new LectureException(INVALID_LECTURE));
+ }
+
+ private Lecture getLectureToStartAttendance(long lectureId, int round) {
+ val lecture = findLecture(lectureId);
+ if (lecture.isEnd()) {
+ throw new LectureException(END_LECTURE);
+ } else if (round == 2 && lecture.isBefore()) {
+ throw new LectureException(NOT_STARTED_PRE_ATTENDANCE);
+ }
+ return lecture;
+ }
+
+ private SubLecture getSubLectureToStartAttendance(Lecture lecture, int round) {
+ return lecture.getSubLectures().stream()
+ .filter(l -> l.getRound() == round)
+ .findFirst()
+ .orElseThrow(() -> new SubLectureException(NO_SUB_LECTURE_EQUAL_ROUND));
+ }
+
+ private Lecture getLectureReadyToEnd(long lectureId) {
+ val lecture = findLecture(lectureId);
+ if (lecture.isNotYetToEnd()) {
+ throw new LectureException(NOT_END_TIME_YET);
+ }
+ if (lecture.isEnd()) {
+ throw new LectureException(END_LECTURE);
+ }
+ return lecture;
+ }
+
+ private void sendAlarm(Lecture lecture) {
+ alarmSender.send(AlarmSenderRequest.of(lecture, valueConfig));
+ }
+
+ private Lecture getLectureToDelete(long lectureId) {
+ val lecture = findLecture(lectureId);
+ if (lecture.isEnd()) {
+ restoreAttendances(lecture.getAttendances());
+ }
+ return lecture;
+ }
+
+ private void restoreAttendances(List attendances) {
+ attendances.forEach(Attendance::restoreMemberScore);
+ }
+
+ private void deleteRelationship(Lecture lecture) {
+ subAttendanceRepository.deleteAllBySubLectureIn(lecture.getSubLectures());
+ subLectureRepository.deleteAllByLecture(lecture);
+ attendanceRepository.deleteByLecture(lecture);
+ }
+
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApi.java b/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApi.java
new file mode 100644
index 00000000..9801990d
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApi.java
@@ -0,0 +1,35 @@
+package org.sopt.makers.operation.web.member.api;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+
+public interface WebMemberApi {
+
+ @Operation(
+ summary = "멤버 리스트 조회 API",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "멤버 리스트 조회 성공"
+ ),
+ @ApiResponse(
+ responseCode = "400",
+ description = "잘못된 요청"
+ ),
+ @ApiResponse(
+ responseCode = "500",
+ description = "서버 내부 오류"
+ )
+ }
+ )
+ ResponseEntity> getMembers(
+ @RequestParam(required = false) Part part,
+ @RequestParam int generation,
+ Pageable pageable);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApiController.java b/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApiController.java
new file mode 100644
index 00000000..0f39709b
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/member/api/WebMemberApiController.java
@@ -0,0 +1,36 @@
+package org.sopt.makers.operation.web.member.api;
+
+import static org.sopt.makers.operation.code.success.web.MemberSuccessCode.*;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.sopt.makers.operation.web.member.service.WebMemberService;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/members")
+public class WebMemberApiController implements WebMemberApi {
+
+ private final WebMemberService memberService;
+
+ @Override
+ @GetMapping("/list")
+ public ResponseEntity> getMembers(
+ @RequestParam(required = false) Part part,
+ @RequestParam int generation,
+ Pageable pageable
+ ) {
+ val response = memberService.getMembers(part, generation, pageable);
+ return ApiResponseUtil.success(SUCCESS_GET_MEMBERS, response);
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/member/dto/response/MemberListGetResponse.java b/operation-api/src/main/java/org/sopt/makers/operation/web/member/dto/response/MemberListGetResponse.java
new file mode 100644
index 00000000..477b28d9
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/member/dto/response/MemberListGetResponse.java
@@ -0,0 +1,71 @@
+package org.sopt.makers.operation.web.member.dto.response;
+
+import static lombok.AccessLevel.*;
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.AttendanceStatus;
+import org.sopt.makers.operation.member.domain.Member;
+
+import lombok.Builder;
+
+@Builder(access = PRIVATE)
+public record MemberListGetResponse(
+ List members,
+ int totalCount
+) {
+ public static MemberListGetResponse of(List memberList, int totalCount) {
+ return MemberListGetResponse.builder()
+ .members(memberList.stream().map(MemberResponse::of).toList())
+ .totalCount(totalCount)
+ .build();
+ }
+
+ @Builder(access = PRIVATE)
+ private record MemberResponse(
+ long id,
+ String name,
+ String university,
+ Part part,
+ float score,
+ AttendanceStatusResponse total
+ ) {
+
+ private static MemberResponse of(Member member) {
+ return MemberResponse.builder()
+ .id(member.getId())
+ .name(member.getName())
+ .university(member.getUniversity())
+ .part(member.getPart())
+ .score(member.getScore())
+ .total(AttendanceStatusResponse.of(member))
+ .build();
+ }
+ }
+
+ @Builder(access = PRIVATE)
+ private record AttendanceStatusResponse(
+ int attendance,
+ int absent,
+ int tardy,
+ int participate
+ ) {
+
+ private static AttendanceStatusResponse of(Member member) {
+ return AttendanceStatusResponse.builder()
+ .attendance(getCount(member, ATTENDANCE))
+ .absent(getCount(member, ABSENT))
+ .tardy(getCount(member, TARDY))
+ .participate(getCount(member, PARTICIPATE))
+ .build();
+ }
+
+ private static int getCount(Member member, AttendanceStatus status) {
+ return (int)member.getAttendances().stream()
+ .filter(attendance -> attendance.getStatus().equals(status))
+ .count();
+ }
+ }
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberService.java b/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberService.java
new file mode 100644
index 00000000..2c8edacb
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberService.java
@@ -0,0 +1,9 @@
+package org.sopt.makers.operation.web.member.service;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.web.member.dto.response.MemberListGetResponse;
+import org.springframework.data.domain.Pageable;
+
+public interface WebMemberService {
+ MemberListGetResponse getMembers(Part part, int generation, Pageable pageable);
+}
diff --git a/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberServiceImpl.java b/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberServiceImpl.java
new file mode 100644
index 00000000..c02a78eb
--- /dev/null
+++ b/operation-api/src/main/java/org/sopt/makers/operation/web/member/service/WebMemberServiceImpl.java
@@ -0,0 +1,26 @@
+package org.sopt.makers.operation.web.member.service;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.member.repository.MemberRepository;
+import org.sopt.makers.operation.web.member.dto.response.MemberListGetResponse;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class WebMemberServiceImpl implements WebMemberService {
+
+ private final MemberRepository memberRepository;
+
+ @Override
+ public MemberListGetResponse getMembers(Part part, int generation, Pageable pageable) {
+ val members = memberRepository.find(generation, part, pageable);
+ val totalCount = memberRepository.count(generation, part);
+ return MemberListGetResponse.of(members, totalCount);
+ }
+}
diff --git a/src/main/resources/application.yml b/operation-api/src/main/resources/application.yml
similarity index 100%
rename from src/main/resources/application.yml
rename to operation-api/src/main/resources/application.yml
diff --git a/src/main/resources/console-appender.xml b/operation-api/src/main/resources/console-appender.xml
similarity index 100%
rename from src/main/resources/console-appender.xml
rename to operation-api/src/main/resources/console-appender.xml
diff --git a/src/main/resources/file-error-appender.xml b/operation-api/src/main/resources/file-error-appender.xml
similarity index 100%
rename from src/main/resources/file-error-appender.xml
rename to operation-api/src/main/resources/file-error-appender.xml
diff --git a/src/main/resources/file-info-appender.xml b/operation-api/src/main/resources/file-info-appender.xml
similarity index 100%
rename from src/main/resources/file-info-appender.xml
rename to operation-api/src/main/resources/file-info-appender.xml
diff --git a/src/main/resources/logback-spring.xml b/operation-api/src/main/resources/logback-spring.xml
similarity index 100%
rename from src/main/resources/logback-spring.xml
rename to operation-api/src/main/resources/logback-spring.xml
diff --git a/operation-auth/build.gradle b/operation-auth/build.gradle
new file mode 100644
index 00000000..8509938c
--- /dev/null
+++ b/operation-auth/build.gradle
@@ -0,0 +1,21 @@
+jar {
+ enabled = true
+}
+
+bootJar {
+ enabled = false
+}
+
+dependencies {
+ implementation project(':operation-common')
+
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+
+ // jwt
+ implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
+ implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
+ implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
+ implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.1'
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
\ No newline at end of file
diff --git a/operation-auth/src/main/java/org/sopt/makers/operation/AuthRoot.java b/operation-auth/src/main/java/org/sopt/makers/operation/AuthRoot.java
new file mode 100644
index 00000000..5046a789
--- /dev/null
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/AuthRoot.java
@@ -0,0 +1,4 @@
+package org.sopt.makers.operation;
+
+public interface AuthRoot {
+}
diff --git a/src/main/java/org/sopt/makers/operation/security/jwt/AdminAuthentication.java b/operation-auth/src/main/java/org/sopt/makers/operation/authentication/AdminAuthentication.java
similarity index 89%
rename from src/main/java/org/sopt/makers/operation/security/jwt/AdminAuthentication.java
rename to operation-auth/src/main/java/org/sopt/makers/operation/authentication/AdminAuthentication.java
index f80aa5d7..bfbca60a 100644
--- a/src/main/java/org/sopt/makers/operation/security/jwt/AdminAuthentication.java
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/authentication/AdminAuthentication.java
@@ -1,11 +1,11 @@
-package org.sopt.makers.operation.security.jwt;
+package org.sopt.makers.operation.authentication;
+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class AdminAuthentication extends UsernamePasswordAuthenticationToken {
-
public AdminAuthentication(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
diff --git a/src/main/java/org/sopt/makers/operation/security/config/SecurityConfig.java b/operation-auth/src/main/java/org/sopt/makers/operation/config/SecurityConfig.java
similarity index 52%
rename from src/main/java/org/sopt/makers/operation/security/config/SecurityConfig.java
rename to operation-auth/src/main/java/org/sopt/makers/operation/config/SecurityConfig.java
index e9209e47..ce3aa176 100644
--- a/src/main/java/org/sopt/makers/operation/security/config/SecurityConfig.java
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/config/SecurityConfig.java
@@ -1,12 +1,12 @@
-package org.sopt.makers.operation.security.config;
+package org.sopt.makers.operation.config;
import lombok.RequiredArgsConstructor;
import lombok.val;
-import org.sopt.makers.operation.security.jwt.JwtAuthenticationFilter;
-import org.sopt.makers.operation.security.jwt.JwtExceptionFilter;
-import org.springframework.beans.factory.annotation.Value;
+import org.sopt.makers.operation.filter.JwtAuthenticationFilter;
+import org.sopt.makers.operation.filter.JwtExceptionFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -14,6 +14,7 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -24,48 +25,56 @@
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;
-
- @Value("${admin.url.prod}")
- private String ADMIN_PROD_URL;
-
- @Value("${admin.url.dev}")
- private String ADMIN_DEV_URL;
-
- @Value("${admin.url.local}")
- private String ADMIN_LOCAL_URL;
+ private final ValueConfig valueConfig;
@Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
+
@Bean
- public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- return http.antMatcher("/**")
- .httpBasic().disable()
+ @Profile("dev")
+ public SecurityFilterChain filterChainDev(HttpSecurity http) throws Exception {
+ http.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
+ .requestMatchers(new AntPathRequestMatcher("/swagger-ui/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/v3/**")).permitAll()
+ );
+ setHttp(http);
+ return http.build();
+ }
+
+ @Bean
+ @Profile("prod")
+ public SecurityFilterChain filterChainProd(HttpSecurity http) throws Exception {
+ setHttp(http);
+ return http.build();
+ }
+
+ private void setHttp(HttpSecurity http) throws Exception {
+ http.httpBasic().disable()
.csrf().disable()
.formLogin().disable()
.cors().configurationSource(corsConfigurationSource())
.and()
- .authorizeRequests()
- .antMatchers("/api/v1/auth/**","/exception/**").permitAll()
- .and()
- .authorizeRequests()
- .antMatchers("/api/v1/**", "/swagger-ui/**").authenticated()
- .and()
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .authorizeHttpRequests(authorizeHttpRequests ->
+ authorizeHttpRequests
+ .requestMatchers(new AntPathRequestMatcher("/api/v1/auth/*")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/api/v1/test/**")).permitAll()
+ .requestMatchers(new AntPathRequestMatcher("/error")).permitAll()
+ .anyRequest().authenticated())
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
- .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class)
- .build();
+ .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
val configuration = new CorsConfiguration();
- configuration.addAllowedOrigin(ADMIN_PROD_URL);
- configuration.addAllowedOrigin(ADMIN_DEV_URL);
- configuration.addAllowedOrigin(ADMIN_LOCAL_URL);
+ configuration.addAllowedOrigin(valueConfig.getADMIN_PROD_URL());
+ configuration.addAllowedOrigin(valueConfig.getADMIN_DEV_URL());
+ configuration.addAllowedOrigin(valueConfig.getADMIN_LOCAL_URL());
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
diff --git a/src/main/java/org/sopt/makers/operation/security/jwt/JwtAuthenticationFilter.java b/operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtAuthenticationFilter.java
similarity index 78%
rename from src/main/java/org/sopt/makers/operation/security/jwt/JwtAuthenticationFilter.java
rename to operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtAuthenticationFilter.java
index 064af05a..7440b615 100644
--- a/src/main/java/org/sopt/makers/operation/security/jwt/JwtAuthenticationFilter.java
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtAuthenticationFilter.java
@@ -1,15 +1,19 @@
-package org.sopt.makers.operation.security.jwt;
+package org.sopt.makers.operation.filter;
+import static org.sopt.makers.operation.code.failure.TokenFailureCode.*;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.val;
-import org.sopt.makers.operation.common.ExceptionMessage;
import org.sopt.makers.operation.exception.TokenException;
+import org.sopt.makers.operation.jwt.JwtTokenProvider;
+import org.sopt.makers.operation.jwt.JwtTokenType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
@@ -25,7 +29,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
val uri = request.getRequestURI();
- if ((uri.startsWith("/api/v1")) && !uri.contains("auth")) {
+ if ((uri.startsWith("/api/v1")) && !uri.contains("auth") && !uri.contains("test")) {
val token = jwtTokenProvider.resolveToken(request);
val jwtTokenType = validateTokenType(request);
@@ -42,7 +46,7 @@ public void doFilterInternal(HttpServletRequest request, HttpServletResponse res
private void checkJwtAvailable (String token, JwtTokenType jwtTokenType) {
if (token == null || !jwtTokenProvider.validateTokenExpiration(token, jwtTokenType)) {
- throw new TokenException(ExceptionMessage.INVALID_AUTH_REQUEST.getName());
+ throw new TokenException(EMPTY_TOKEN);
}
}
diff --git a/operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtExceptionFilter.java b/operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtExceptionFilter.java
new file mode 100644
index 00000000..a0517f11
--- /dev/null
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/filter/JwtExceptionFilter.java
@@ -0,0 +1,46 @@
+package org.sopt.makers.operation.filter;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.sopt.makers.operation.exception.TokenException;
+import org.sopt.makers.operation.util.ApiResponseUtil;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.val;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+
+@Component
+public class JwtExceptionFilter extends OncePerRequestFilter {
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest httpServletRequest,
+ HttpServletResponse httpServletResponse,
+ FilterChain filterChain
+ ) throws ServletException, IOException {
+ try {
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
+ } catch(TokenException e) {
+ val objectMapper = new ObjectMapper();
+ val jsonResponse = objectMapper.writeValueAsString(getFailureResponse(e.getFailureCode()));
+
+ httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
+ httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ httpServletResponse.setCharacterEncoding("UTF-8");
+ httpServletResponse.getWriter().write(jsonResponse);
+ }
+ }
+
+ private ResponseEntity> getFailureResponse(FailureCode failureCode) {
+ return ApiResponseUtil.failure(failureCode);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenProvider.java b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java
similarity index 80%
rename from src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenProvider.java
rename to operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java
index ae7d974a..ae249d01 100644
--- a/src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenProvider.java
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenProvider.java
@@ -1,23 +1,25 @@
-package org.sopt.makers.operation.security.jwt;
+package org.sopt.makers.operation.jwt;
+
+import static org.sopt.makers.operation.code.failure.TokenFailureCode.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.SignatureException;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.val;
-import org.sopt.makers.operation.common.ExceptionMessage;
+import org.sopt.makers.operation.authentication.AdminAuthentication;
import org.sopt.makers.operation.exception.TokenException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.crypto.spec.SecretKeySpec;
-import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
+
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -27,7 +29,6 @@
import java.util.Map;
@RequiredArgsConstructor
-@Transactional(readOnly = true)
@Service
public class JwtTokenProvider {
@@ -40,7 +41,7 @@ public class JwtTokenProvider {
@Value("${spring.jwt.secretKey.app}")
private String appAccessSecretKey;
- public String generateAccessToken(Authentication authentication) {
+ public String generateAccessToken(final Authentication authentication) {
val encodedKey = encodeKey(accessSecretKey);
val secretKeyBytes = DatatypeConverter.parseBase64Binary(encodedKey);
val accessKey = new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS256.getJcaName());
@@ -53,7 +54,7 @@ public String generateAccessToken(Authentication authentication) {
.compact();
}
- public String generateRefreshToken(Authentication authentication) {
+ public String generateRefreshToken(final Authentication authentication) {
val encodedKey = encodeKey(refreshSecretKey);
val secretKeyBytes = DatatypeConverter.parseBase64Binary(encodedKey);
val refreshKey = new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS256.getJcaName());
@@ -70,10 +71,8 @@ public boolean validateTokenExpiration(String token, JwtTokenType jwtTokenType)
try {
getClaimsFromToken(token, jwtTokenType);
return true;
- } catch (ExpiredJwtException e) {
- throw new TokenException(ExceptionMessage.EXPIRED_TOKEN.getName());
- } catch (SignatureException e) {
- throw new TokenException(ExceptionMessage.INVALID_SIGNATURE.getName());
+ } catch (ExpiredJwtException | SignatureException e) {
+ return false;
}
}
@@ -89,10 +88,8 @@ public Long getPlayGroundId(String token, JwtTokenType jwtTokenType) {
val claims = getClaimsFromToken(token, jwtTokenType);
return Long.parseLong(claims.get("playgroundId").toString());
- } catch (ExpiredJwtException e) {
- throw new TokenException(ExceptionMessage.EXPIRED_TOKEN.getName());
- } catch (SignatureException e) {
- throw new TokenException(ExceptionMessage.INVALID_SIGNATURE.getName());
+ } catch (ExpiredJwtException | SignatureException e) {
+ throw new TokenException(INVALID_TOKEN);
}
}
@@ -101,10 +98,8 @@ public Long getId(String token, JwtTokenType jwtTokenType) {
val claims = getClaimsFromToken(token, jwtTokenType);
return Long.parseLong(claims.getSubject());
- } catch (ExpiredJwtException e) {
- throw new TokenException(ExceptionMessage.EXPIRED_TOKEN.getName());
- } catch (SecurityException e) {
- throw new TokenException(ExceptionMessage.INVALID_SIGNATURE.getName());
+ } catch (ExpiredJwtException | SignatureException e) {
+ throw new TokenException(INVALID_TOKEN);
}
}
@@ -143,7 +138,7 @@ private LocalDateTime setExpireTime(LocalDateTime now, JwtTokenType jwtTokenType
return switch (jwtTokenType) {
case ACCESS_TOKEN -> now.plusHours(5);
case REFRESH_TOKEN -> now.plusWeeks(2);
- case APP_ACCESS_TOKEN -> throw new TokenException(ExceptionMessage.INVALID_TOKEN.getName());
+ case APP_ACCESS_TOKEN -> throw new TokenException(INVALID_TOKEN);
};
}
diff --git a/src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenType.java b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java
similarity index 62%
rename from src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenType.java
rename to operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java
index 753724db..385ec839 100644
--- a/src/main/java/org/sopt/makers/operation/security/jwt/JwtTokenType.java
+++ b/operation-auth/src/main/java/org/sopt/makers/operation/jwt/JwtTokenType.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.security.jwt;
+package org.sopt.makers.operation.jwt;
public enum JwtTokenType {
ACCESS_TOKEN, REFRESH_TOKEN, APP_ACCESS_TOKEN
diff --git a/operation-common/build.gradle b/operation-common/build.gradle
new file mode 100644
index 00000000..4712a383
--- /dev/null
+++ b/operation-common/build.gradle
@@ -0,0 +1,11 @@
+jar {
+ enabled = true
+}
+
+bootJar {
+ enabled = false
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
\ No newline at end of file
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/CommonRoot.java b/operation-common/src/main/java/org/sopt/makers/operation/CommonRoot.java
new file mode 100644
index 00000000..c2e06e1f
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/CommonRoot.java
@@ -0,0 +1,4 @@
+package org.sopt.makers.operation;
+
+public interface CommonRoot {
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AlarmFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AlarmFailureCode.java
new file mode 100644
index 00000000..ce9ae1ac
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AlarmFailureCode.java
@@ -0,0 +1,21 @@
+package org.sopt.makers.operation.code.failure;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum AlarmFailureCode implements FailureCode {
+ FAIL_SEND_ALARM(BAD_REQUEST, "알림 전송에 실패하였습니다."),
+ SENT_ALARM(BAD_REQUEST, "전송된 알림입니다."),
+ INVALID_ALARM(NOT_FOUND, "알림이 존재하지 않습니다."),
+ FAIL_INACTIVE_USERS(BAD_REQUEST, "비활동 유저 불러오기에 실패하였습니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AttendanceFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AttendanceFailureCode.java
new file mode 100644
index 00000000..d39fe3d1
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/AttendanceFailureCode.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.code.failure;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum AttendanceFailureCode implements FailureCode {
+ INVALID_ATTENDANCE(NOT_FOUND, "존재하지 않는 출석 세션입니다."),
+ INVALID_SUB_ATTENDANCE(NOT_FOUND, "존재하지 않는 N차 출석입니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/FailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/FailureCode.java
new file mode 100644
index 00000000..5363d453
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/FailureCode.java
@@ -0,0 +1,8 @@
+package org.sopt.makers.operation.code.failure;
+
+import org.springframework.http.HttpStatus;
+
+public interface FailureCode {
+ HttpStatus getStatus();
+ String getMessage();
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/LectureFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/LectureFailureCode.java
new file mode 100644
index 00000000..c23d10a8
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/LectureFailureCode.java
@@ -0,0 +1,30 @@
+package org.sopt.makers.operation.code.failure;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum LectureFailureCode implements FailureCode {
+ INVALID_DATE_PATTERN(BAD_REQUEST, "유효하지 않은 날짜 형식입니다."),
+ INVALID_SUB_LECTURE(NOT_FOUND, "존재하지 않는 세션입니다."),
+ INVALID_CODE(BAD_REQUEST, "코드가 일치하지 않아요!"),
+ NOT_STARTED_NTH_ATTENDANCE(BAD_REQUEST, "차 출석 시작 전입니다."),
+ ENDED_ATTENDANCE(BAD_REQUEST, "차 출석이 이미 종료되었습니다."),
+ INVALID_LECTURE(NOT_FOUND, "존재하지 않는 세션입니다."),
+ NOT_END_TIME_YET(BAD_REQUEST, "세션 종료 시간이 지나지 않았습니다."),
+ NO_SUB_LECTURE_EQUAL_ROUND(NOT_FOUND, "해당 라운드와 일치하는 출석 세션이 없습니다."),
+ END_LECTURE(BAD_REQUEST, "이미 종료된 세션입니다."),
+ NOT_STARTED_PRE_ATTENDANCE(BAD_REQUEST, "이전의 출석체크가 시작되지 않았습니다."),
+ INVALID_COUNT_SESSION(BAD_REQUEST, "세션의 개수가 올바르지 않습니다."),
+ NO_SESSION(NOT_FOUND, "오늘 세션이 없습니다."),
+ NOT_STARTED_ATTENDANCE(BAD_REQUEST, "출석 시작 전입니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/MemberFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/MemberFailureCode.java
new file mode 100644
index 00000000..fcb0ef99
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/MemberFailureCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.failure;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum MemberFailureCode implements FailureCode {
+ INVALID_MEMBER(NOT_FOUND, "존재하지 않는 회원입니다.")
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/ScheduleFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/ScheduleFailureCode.java
new file mode 100644
index 00000000..7d4ffca5
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/ScheduleFailureCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.failure;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum ScheduleFailureCode implements FailureCode {
+ INVALID_DATE_PERM(BAD_REQUEST, "조회할 날짜 기간은 50일을 넘길 수 없습니다.");
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/TokenFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/TokenFailureCode.java
new file mode 100644
index 00000000..5713fec5
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/TokenFailureCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.failure;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@RequiredArgsConstructor
+@Getter
+public enum TokenFailureCode implements FailureCode {
+ EMPTY_TOKEN(BAD_REQUEST, "빈 토큰입니다."),
+ INVALID_TOKEN(BAD_REQUEST, "유효하지 않은 토큰입니다.")
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/admin/AdminFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/admin/AdminFailureCode.java
new file mode 100644
index 00000000..eeadbd2d
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/admin/AdminFailureCode.java
@@ -0,0 +1,22 @@
+package org.sopt.makers.operation.code.failure.admin;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum AdminFailureCode implements FailureCode {
+ DUPLICATED_EMAIL(BAD_REQUEST,"중복되는 이메일입니다."),
+ INVALID_EMAIL(BAD_REQUEST,"이메일이 존재하지 않습니다."),
+ INVALID_PASSWORD(BAD_REQUEST,"비밀번호가 일치하지 않습니다."),
+ NOT_APPROVED_ACCOUNT(BAD_REQUEST,"승인되지 않은 계정입니다."),
+ INVALID_REFRESH_TOKEN(BAD_REQUEST,"유효하지 않은 리프레시 토큰입니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/attendance/AttendanceFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/attendance/AttendanceFailureCode.java
new file mode 100644
index 00000000..1c35e1ed
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/attendance/AttendanceFailureCode.java
@@ -0,0 +1,17 @@
+package org.sopt.makers.operation.code.failure.attendance;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum AttendanceFailureCode implements FailureCode {
+ INVALID_ATTENDANCE(BAD_REQUEST, "존재하지 않는 출석 세션입니다.");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/lecture/LectureFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/lecture/LectureFailureCode.java
new file mode 100644
index 00000000..ef9d4d23
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/lecture/LectureFailureCode.java
@@ -0,0 +1,26 @@
+package org.sopt.makers.operation.code.failure.lecture;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum LectureFailureCode implements FailureCode {
+ NOT_STARTED_NTH_ATTENDANCE(BAD_REQUEST, "차 출석 시작 전입니다."),
+ INVALID_ATTENDANCE(BAD_REQUEST,"존재하지 않는 출석 세션입니다."),
+ ENDED_ATTENDANCE(BAD_REQUEST, "차 출석이 이미 종료되었습니다."),
+ ENDED_FIRST_ATTENDANCE(BAD_REQUEST, "1차 출석이 이미 종료되었습니다."),
+ ENDED_SECOND_ATTENDANCE(BAD_REQUEST, "차 출석이 이미 종료되었습니다."),
+ INVALID_COUNT_SESSION(BAD_REQUEST,"세션의 개수가 올바르지 않습니다."),
+ INVALID_LECTURE(BAD_REQUEST,"존재하지 않는 세션입니다."),
+ NO_SESSION(BAD_REQUEST,"오늘 세션이 없습니다."),
+ NOT_STARTED_ATTENDANCE(BAD_REQUEST,"출석 시작 전입니다."),
+ END_LECTURE(BAD_REQUEST,"이미 종료된 세션입니다.");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/member/memberFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/member/memberFailureCode.java
new file mode 100644
index 00000000..7e9fc0bc
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/member/memberFailureCode.java
@@ -0,0 +1,17 @@
+package org.sopt.makers.operation.code.failure.member;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum memberFailureCode implements FailureCode {
+ INVALID_MEMBER(BAD_REQUEST, "존재하지 않는 회원입니다.");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subAttendance/subAttendanceFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subAttendance/subAttendanceFailureCode.java
new file mode 100644
index 00000000..b024a7e7
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subAttendance/subAttendanceFailureCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.failure.subAttendance;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum subAttendanceFailureCode implements FailureCode {
+ INVALID_SUB_LECTURE(BAD_REQUEST, "존재하지 않는 세션입니다."),
+ INVALID_CODE(BAD_REQUEST, "코드가 일치하지 않아요!");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subLecture/subLectureFailureCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subLecture/subLectureFailureCode.java
new file mode 100644
index 00000000..5da961ab
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/failure/subLecture/subLectureFailureCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.failure.subLecture;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+
+@Getter
+@RequiredArgsConstructor
+public enum subLectureFailureCode implements FailureCode {
+ INVALID_SUB_ATTENDANCE(BAD_REQUEST, "존재하지 않는 N차 출석입니다."),
+ NO_SUB_LECTURE_EQUAL_ROUND(BAD_REQUEST,"해당 라운드와 일치하는 출석 세션이 없습니다.");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/SuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/SuccessCode.java
new file mode 100644
index 00000000..e4575f0e
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/SuccessCode.java
@@ -0,0 +1,8 @@
+package org.sopt.makers.operation.code.success;
+
+import org.springframework.http.HttpStatus;
+
+public interface SuccessCode {
+ HttpStatus getStatus();
+ String getMessage();
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/AttendanceSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/AttendanceSuccessCode.java
new file mode 100644
index 00000000..67b66bd5
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/AttendanceSuccessCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.success.app;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum AttendanceSuccessCode implements SuccessCode {
+ SUCCESS_ATTEND(OK, "출석 성공");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/LectureSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/LectureSuccessCode.java
new file mode 100644
index 00000000..c5ac68a8
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/LectureSuccessCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.success.app;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.OK;
+
+@Getter
+@RequiredArgsConstructor
+public enum LectureSuccessCode implements SuccessCode {
+ SUCCESS_SINGLE_GET_LECTURE(OK,"세션 조회 성공"),
+ SUCCESS_GET_LECTURE_ROUND(OK,"출석 차수 조회 성공");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/MemberSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/MemberSuccessCode.java
new file mode 100644
index 00000000..19513fba
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/MemberSuccessCode.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.code.success.app;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.OK;
+
+@Getter
+@RequiredArgsConstructor
+public enum MemberSuccessCode implements SuccessCode {
+ SUCCESS_GET_TOTAL_ATTENDANCE(OK,"전체 출석정보 조회 성공"),
+ SUCCESS_GET_ATTENDANCE_SCORE(OK,"출석 점수 조회 성공");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/ScheduleSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/ScheduleSuccessCode.java
new file mode 100644
index 00000000..a6cf8515
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/app/ScheduleSuccessCode.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.code.success.app;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum ScheduleSuccessCode implements SuccessCode {
+ SUCCESS_GET_SCHEDULES(OK, "일정 리스트 조회 성공"),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AdminSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AdminSuccessCode.java
new file mode 100644
index 00000000..cf6d82e3
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AdminSuccessCode.java
@@ -0,0 +1,20 @@
+package org.sopt.makers.operation.code.success.web;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import static org.springframework.http.HttpStatus.OK;
+
+@Getter
+@RequiredArgsConstructor
+public enum AdminSuccessCode implements SuccessCode {
+ SUCCESS_ATTEND(OK, "출석 성공"),
+ SUCCESS_SIGN_UP(OK,"회원 가입 성공"),
+ SUCCESS_LOGIN_UP(OK,"로그인 성공"),
+ SUCCESS_GET_REFRESH_TOKEN(OK,"토큰 재발급 성공");
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AlarmSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AlarmSuccessCode.java
new file mode 100644
index 00000000..f1526ee2
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AlarmSuccessCode.java
@@ -0,0 +1,23 @@
+package org.sopt.makers.operation.code.success.web;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum AlarmSuccessCode implements SuccessCode {
+ SUCCESS_SEND_ALARM(OK, "알림 전송 성공"),
+ SUCCESS_CREATE_ALARM(CREATED, "알림 생성 성공"),
+ SUCCESS_GET_ALARMS(OK, "알림 리스트 조회 성공"),
+ SUCCESS_GET_ALARM(OK, "알림 상세 조회 성공"),
+ SUCCESS_DELETE_ALARM(OK, "알림 삭제 성공"),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AttendanceSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AttendanceSuccessCode.java
new file mode 100644
index 00000000..21c8e634
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/AttendanceSuccessCode.java
@@ -0,0 +1,22 @@
+package org.sopt.makers.operation.code.success.web;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum AttendanceSuccessCode implements SuccessCode {
+ SUCCESS_UPDATE_ATTENDANCE_STATUS(OK, "출석 상태 변경 성공"),
+ SUCCESS_GET_MEMBER_ATTENDANCE(OK, "회원 출석 정보 조회 성공"),
+ SUCCESS_UPDATE_MEMBER_SCORE(OK, "회원 출석 점수 갱신 성공"),
+ SUCCESS_GET_ATTENDANCES(OK, "출석 리스트 조회 성공"),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/LectureSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/LectureSuccessCode.java
new file mode 100644
index 00000000..beeee546
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/LectureSuccessCode.java
@@ -0,0 +1,26 @@
+package org.sopt.makers.operation.code.success.web;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum LectureSuccessCode implements SuccessCode {
+ SUCCESS_CREATE_LECTURE(CREATED, "세션 생성 성공"),
+ SUCCESS_GET_LECTURES(OK, "세션 리스트 조회 성공"),
+ SUCCESS_GET_LECTURE(OK, "세션 상세 조회 성공"),
+ SUCCESS_START_ATTENDANCE(CREATED, "출석 시작 성공"),
+ SUCCESS_GET_MEMBERS(OK, "유저 리스트 조회 성공"),
+ SUCCESS_DELETE_LECTURE(OK, "세션 삭제 성공"),
+ SUCCESS_UPDATE_MEMBER_SCORE(OK, "회원 출석 점수 갱신 성공"),
+ SUCCESS_END_LECTURE(OK, "세션 종료 성공"),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/MemberSuccessCode.java b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/MemberSuccessCode.java
new file mode 100644
index 00000000..1bb0ea65
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/code/success/web/MemberSuccessCode.java
@@ -0,0 +1,19 @@
+package org.sopt.makers.operation.code.success.web;
+
+import static org.springframework.http.HttpStatus.*;
+
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.springframework.http.HttpStatus;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Getter
+public enum MemberSuccessCode implements SuccessCode {
+ SUCCESS_GET_MEMBERS(OK, "유저 리스트 조회 성공"),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/config/ValueConfig.java b/operation-common/src/main/java/org/sopt/makers/operation/config/ValueConfig.java
new file mode 100644
index 00000000..90756781
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/config/ValueConfig.java
@@ -0,0 +1,61 @@
+package org.sopt.makers.operation.config;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Getter;
+
+@Configuration
+@Getter
+public class ValueConfig {
+
+ @Value("${sopt.alarm.message.title_end}")
+ private String ALARM_MESSAGE_TITLE;
+ @Value("${sopt.alarm.message.content_end}")
+ private String ALARM_MESSAGE_CONTENT;
+ @Value("${sopt.current.generation}")
+ private int GENERATION;
+ @Value("${admin.url.prod}")
+ private String ADMIN_PROD_URL;
+ @Value("${admin.url.dev}")
+ private String ADMIN_DEV_URL;
+ @Value("${admin.url.local}")
+ private String ADMIN_LOCAL_URL;
+ @Value("${notification.key}")
+ private String NOTIFICATION_KEY;
+ @Value("${notification.url}")
+ private String NOTIFICATION_URL;
+ @Value("${sopt.makers.playground.server}")
+ private String playGroundURI;
+ @Value("${sopt.makers.playground.token}")
+ private String playGroundToken;
+
+ private final int SUB_LECTURE_MAX_ROUND = 2;
+ private final String ETC_MESSAGE = "출석 점수가 반영되지 않아요.";
+ private final String SEMINAR_MESSAGE = "";
+ private final String EVENT_MESSAGE = "행사도 참여하고, 출석점수도 받고, 일석이조!";
+ private final String SWAGGER_URI = "/swagger-ui/**";
+ private final int ATTENDANCE_MINUTE = 10;
+ private final int MIN_SCHEDULE_DURATION = 1;
+ private final int MAX_SCHEDULE_DURATION = 50;
+ private final int DAY_DURATION = 1;
+ private final int TWO_DAYS_DURATION = 2;
+
+ private final List APP_LINK_LIST = Arrays.asList(
+ "home",
+ "home/notification",
+ "home/mypage",
+ "home/attendance",
+ "home/attendance/attendance-modal",
+ "home/soptamp",
+ "home/soptamp/entire-ranking",
+ "home/soptamp/current-generation-ranking"
+ );
+ private final List WEB_LINK_LIST = Arrays.asList(
+ "https://playground.sopt.org/members",
+ "https://playground.sopt.org/group"
+ );
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/dto/BaseResponse.java b/operation-common/src/main/java/org/sopt/makers/operation/dto/BaseResponse.java
new file mode 100644
index 00000000..4fb2c888
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/dto/BaseResponse.java
@@ -0,0 +1,32 @@
+package org.sopt.makers.operation.dto;
+
+import static com.fasterxml.jackson.annotation.JsonInclude.Include.*;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import lombok.AccessLevel;
+import lombok.Builder;
+
+@Builder(access = AccessLevel.PRIVATE)
+public record BaseResponse (
+ boolean success,
+ String message,
+ @JsonInclude(value = NON_NULL)
+ T data
+) {
+
+ public static BaseResponse> of(String message, T data) {
+ return BaseResponse.builder()
+ .success(true)
+ .message(message)
+ .data(data)
+ .build();
+ }
+
+ public static BaseResponse> of(boolean isSuccess, String message) {
+ return BaseResponse.builder()
+ .success(isSuccess)
+ .message(message)
+ .build();
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/AdminFailureException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/AdminFailureException.java
new file mode 100644
index 00000000..d19abe71
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/AdminFailureException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class AdminFailureException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public AdminFailureException(FailureCode failureCode) {
+ super("[AuthFailureException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/AlarmException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/AlarmException.java
new file mode 100644
index 00000000..c31cf79b
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/AlarmException.java
@@ -0,0 +1,17 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.AlarmFailureCode;
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class AlarmException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public AlarmException(AlarmFailureCode failureCode) {
+ super("[AlarmException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/AttendanceException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/AttendanceException.java
new file mode 100644
index 00000000..6b374182
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/AttendanceException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class AttendanceException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public AttendanceException(FailureCode failureCode) {
+ super("[AttendanceException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/DateTimeParseCustomException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/DateTimeParseCustomException.java
new file mode 100644
index 00000000..3a129a4e
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/DateTimeParseCustomException.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.exception;
+
+import java.time.format.DateTimeParseException;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class DateTimeParseCustomException extends DateTimeParseException {
+
+ private final FailureCode failureCode;
+
+ public DateTimeParseCustomException(FailureCode failureCode, String input, int index) {
+ super("[DateTimeParseException] : " + failureCode.getMessage(), input, index);
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/LectureException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/LectureException.java
new file mode 100644
index 00000000..5e05d9e0
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/LectureException.java
@@ -0,0 +1,32 @@
+package org.sopt.makers.operation.exception;
+
+import static org.sopt.makers.operation.code.failure.lecture.LectureFailureCode.*;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.sopt.makers.operation.code.failure.lecture.LectureFailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class LectureException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public LectureException(FailureCode failureCode) {
+ super("[LectureException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+
+ public LectureException(FailureCode failureCode, int round) {
+ super("[LectureException] : " + failureCode.getMessage());
+ this.failureCode = getFailureCodeByRound(round);
+ }
+
+ private LectureFailureCode getFailureCodeByRound(int round) {
+ return switch (round) {
+ case 1 -> ENDED_FIRST_ATTENDANCE;
+ case 2 -> ENDED_SECOND_ATTENDANCE;
+ default-> ENDED_ATTENDANCE;
+ };
+ }
+}
\ No newline at end of file
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/MemberException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/MemberException.java
new file mode 100644
index 00000000..7e0209f6
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/MemberException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class MemberException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public MemberException(FailureCode failureCode) {
+ super("[MemberException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/ScheduleException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/ScheduleException.java
new file mode 100644
index 00000000..579e2506
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/ScheduleException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class ScheduleException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public ScheduleException(FailureCode failureCode) {
+ super("[ScheduleException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/SubLectureException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/SubLectureException.java
new file mode 100644
index 00000000..881dc269
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/SubLectureException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class SubLectureException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public SubLectureException(FailureCode failureCode) {
+ super("[SubLectureException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/exception/TokenException.java b/operation-common/src/main/java/org/sopt/makers/operation/exception/TokenException.java
new file mode 100644
index 00000000..dc555a28
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/exception/TokenException.java
@@ -0,0 +1,16 @@
+package org.sopt.makers.operation.exception;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+
+import lombok.Getter;
+
+@Getter
+public class TokenException extends RuntimeException {
+
+ private final FailureCode failureCode;
+
+ public TokenException(FailureCode failureCode) {
+ super("[TokenException] : " + failureCode.getMessage());
+ this.failureCode = failureCode;
+ }
+}
diff --git a/operation-common/src/main/java/org/sopt/makers/operation/util/ApiResponseUtil.java b/operation-common/src/main/java/org/sopt/makers/operation/util/ApiResponseUtil.java
new file mode 100644
index 00000000..aa70a45e
--- /dev/null
+++ b/operation-common/src/main/java/org/sopt/makers/operation/util/ApiResponseUtil.java
@@ -0,0 +1,35 @@
+package org.sopt.makers.operation.util;
+
+import org.sopt.makers.operation.code.failure.FailureCode;
+import org.sopt.makers.operation.code.success.SuccessCode;
+import org.sopt.makers.operation.dto.BaseResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+
+public interface ApiResponseUtil {
+
+ static ResponseEntity> success(SuccessCode code, T data) {
+ return ResponseEntity
+ .status(code.getStatus())
+ .body(BaseResponse.of(code.getMessage(), data));
+ }
+
+ static ResponseEntity> success(SuccessCode code) {
+ return ResponseEntity
+ .status(code.getStatus())
+ .body(BaseResponse.of(true, code.getMessage()));
+ }
+
+ static ResponseEntity> success(SuccessCode code, HttpHeaders headers, T data) {
+ return ResponseEntity
+ .status(code.getStatus())
+ .headers(headers)
+ .body(BaseResponse.of(code.getMessage(), data));
+ }
+
+ static ResponseEntity> failure(FailureCode code) {
+ return ResponseEntity
+ .status(code.getStatus())
+ .body(BaseResponse.of(false, code.getMessage()));
+ }
+}
diff --git a/operation-domain/build.gradle b/operation-domain/build.gradle
new file mode 100644
index 00000000..88f53245
--- /dev/null
+++ b/operation-domain/build.gradle
@@ -0,0 +1,31 @@
+jar {
+ enabled = true
+}
+
+bootJar {
+ enabled = false
+}
+
+dependencies {
+ implementation project(path: ':operation-common')
+
+ // QueryDSL
+// implementation "com.querydsl:querydsl-jpa:5.0.0"
+// implementation "com.querydsl:querydsl-apt:5.0.0"
+// testImplementation 'junit:junit:4.13.1'
+// testImplementation 'junit:junit:4.13.1'
+ implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+ annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
+ annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+ annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+
+ // jpa
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+
+ // db
+ runtimeOnly 'com.h2database:h2'
+ runtimeOnly 'org.postgresql:postgresql'
+
+ // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
+ implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.14.0-rc1'
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/DomainRoot.java b/operation-domain/src/main/java/org/sopt/makers/operation/DomainRoot.java
new file mode 100644
index 00000000..8b4c7f74
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/DomainRoot.java
@@ -0,0 +1,4 @@
+package org.sopt.makers.operation;
+
+public interface DomainRoot {
+}
diff --git a/src/main/java/org/sopt/makers/operation/entity/Admin.java b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Admin.java
similarity index 66%
rename from src/main/java/org/sopt/makers/operation/entity/Admin.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Admin.java
index f6fd1bef..0e09590e 100644
--- a/src/main/java/org/sopt/makers/operation/entity/Admin.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Admin.java
@@ -1,21 +1,24 @@
-package org.sopt.makers.operation.entity;
-
+package org.sopt.makers.operation.admin.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import javax.persistence.*;
-
-import static javax.persistence.GenerationType.IDENTITY;
-
@Getter
@Setter
@Entity
@NoArgsConstructor
public class Admin {
@Id
- @GeneratedValue(strategy = IDENTITY)
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "admin_id")
private Long id;
@@ -49,4 +52,8 @@ public Admin(String email, String password, String name, Role role) {
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
+
+ public boolean isNotAllowed() {
+ return this.status.equals(AdminStatus.NOT_CERTIFIED);
+ }
}
diff --git a/src/main/java/org/sopt/makers/operation/entity/AdminStatus.java b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/AdminStatus.java
similarity index 60%
rename from src/main/java/org/sopt/makers/operation/entity/AdminStatus.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/AdminStatus.java
index 980530d3..74acd24c 100644
--- a/src/main/java/org/sopt/makers/operation/entity/AdminStatus.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/AdminStatus.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.admin.domain;
public enum AdminStatus {
DEVELOPER, SOPT, MAKERS, NOT_CERTIFIED
diff --git a/src/main/java/org/sopt/makers/operation/entity/Role.java b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Role.java
similarity index 74%
rename from src/main/java/org/sopt/makers/operation/entity/Role.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Role.java
index a9709150..0da770ba 100644
--- a/src/main/java/org/sopt/makers/operation/entity/Role.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/admin/domain/Role.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.admin.domain;
public enum Role {
OPERATION_TEAM, PRESIDENT, VICE_PRESIDENT, AFFAIRS, MANAGE, MEDIA, PLAN, DESIGN, WEB, ANDROID, IOS, SERVER, MAKERS
diff --git a/src/main/java/org/sopt/makers/operation/repository/AdminRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/admin/repository/AdminRepository.java
similarity index 54%
rename from src/main/java/org/sopt/makers/operation/repository/AdminRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/admin/repository/AdminRepository.java
index 7bb0beac..f520b31a 100644
--- a/src/main/java/org/sopt/makers/operation/repository/AdminRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/admin/repository/AdminRepository.java
@@ -1,14 +1,14 @@
-package org.sopt.makers.operation.repository;
+package org.sopt.makers.operation.admin.repository;
-import org.sopt.makers.operation.entity.Admin;
+import java.util.Optional;
+
+import org.sopt.makers.operation.admin.domain.Admin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
-import java.util.Optional;
-
@Repository
public interface AdminRepository extends JpaRepository {
- Optional findByEmail(String email);
- boolean existsByEmail(String email);
+ Optional findByEmail(String email);
+ boolean existsByEmail(String email);
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Alarm.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Alarm.java
new file mode 100644
index 00000000..9c33824b
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Alarm.java
@@ -0,0 +1,116 @@
+package org.sopt.makers.operation.alarm.domain;
+
+import static java.util.Objects.*;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.sopt.makers.operation.common.domain.BaseEntity;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.schedule.converter.StringListConverter;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor
+@Getter
+public class Alarm extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "alarm_id")
+ private Long id;
+
+ private int generation;
+
+ private int generationAt;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private Attribute attribute;
+
+ private String title;
+
+ @Column(columnDefinition = "TEXT")
+ private String content;
+
+ private String link;
+
+ private Boolean isActive;
+
+ @Enumerated(value = EnumType.STRING)
+ private Part part;
+
+ @Column(columnDefinition = "TEXT", nullable = false)
+ @Convert(converter = StringListConverter.class)
+ private List targetList;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private Status status;
+
+ private LocalDateTime sendAt;
+
+ @Builder
+ public Alarm(
+ int generation,
+ int generationAt,
+ Attribute attribute,
+ String title,
+ String content,
+ String link,
+ Boolean isActive,
+ Part part,
+ List targetList
+ ) {
+ this.generation = generation;
+ this.generationAt = generationAt;
+ this.attribute = attribute;
+ this.title = title;
+ this.content = content;
+ setLink(link);
+ setTargetsInfo(isActive, part, targetList);
+ this.status = Status.BEFORE;
+ }
+
+ private void setLink(String link) {
+ if (nonNull(link)) {
+ this.link = link;
+ }
+ }
+
+ private void setTargetsInfo(Boolean isActive, Part part, List targetList) {
+ if (nonNull(targetList)) {
+ this.targetList = targetList;
+ } else {
+ this.isActive = isActive;
+ this.part = part;
+ this.targetList = new ArrayList<>();
+ }
+ }
+
+ public boolean isSent() {
+ return this.status.equals(Status.AFTER);
+ }
+
+ public void updateToSent() {
+ this.status = Status.AFTER;
+ this.sendAt = LocalDateTime.now();
+ }
+
+ public boolean hasTargets() {
+ return Objects.isNull(this.isActive) || Objects.isNull(this.part);
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/entity/alarm/Attribute.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Attribute.java
similarity index 78%
rename from src/main/java/org/sopt/makers/operation/entity/alarm/Attribute.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Attribute.java
index 0a64f842..86a15275 100644
--- a/src/main/java/org/sopt/makers/operation/entity/alarm/Attribute.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Attribute.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity.alarm;
+package org.sopt.makers.operation.alarm.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/src/main/java/org/sopt/makers/operation/entity/alarm/Status.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Status.java
similarity index 79%
rename from src/main/java/org/sopt/makers/operation/entity/alarm/Status.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Status.java
index 682a4247..5f34a054 100644
--- a/src/main/java/org/sopt/makers/operation/entity/alarm/Status.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/domain/Status.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity.alarm;
+package org.sopt.makers.operation.alarm.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmCustomRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmCustomRepository.java
new file mode 100644
index 00000000..daa0c892
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmCustomRepository.java
@@ -0,0 +1,13 @@
+package org.sopt.makers.operation.alarm.repository;
+
+import java.util.List;
+
+import org.sopt.makers.operation.alarm.domain.Alarm;
+import org.sopt.makers.operation.alarm.domain.Status;
+import org.sopt.makers.operation.common.domain.Part;
+import org.springframework.data.domain.Pageable;
+
+public interface AlarmCustomRepository {
+ List findOrderByCreatedDate(Integer generation, Part part, Status status, Pageable pageable);
+ int count(int generation, Part part, Status status);
+}
diff --git a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepository.java
similarity index 60%
rename from src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepository.java
index 9ca5e9d4..216a5e78 100644
--- a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepository.java
@@ -1,6 +1,6 @@
-package org.sopt.makers.operation.repository.alarm;
+package org.sopt.makers.operation.alarm.repository;
-import org.sopt.makers.operation.entity.alarm.Alarm;
+import org.sopt.makers.operation.alarm.domain.Alarm;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AlarmRepository extends JpaRepository, AlarmCustomRepository {
diff --git a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepositoryImpl.java
similarity index 73%
rename from src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepositoryImpl.java
index bacbba7c..7924b6f1 100644
--- a/src/main/java/org/sopt/makers/operation/repository/alarm/AlarmRepositoryImpl.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/alarm/repository/AlarmRepositoryImpl.java
@@ -1,13 +1,13 @@
-package org.sopt.makers.operation.repository.alarm;
+package org.sopt.makers.operation.alarm.repository;
import static java.util.Objects.*;
-import static org.sopt.makers.operation.entity.alarm.QAlarm.*;
+import static org.sopt.makers.operation.alarm.domain.QAlarm.*;
import java.util.List;
-import org.sopt.makers.operation.entity.Part;
-import org.sopt.makers.operation.entity.alarm.Alarm;
-import org.sopt.makers.operation.entity.alarm.Status;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.alarm.domain.Alarm;
+import org.sopt.makers.operation.alarm.domain.Status;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
@@ -22,7 +22,7 @@ public class AlarmRepositoryImpl implements AlarmCustomRepository {
private final JPAQueryFactory queryFactory;
@Override
- public List getAlarms(Integer generation, Part part, Status status, Pageable pageable) {
+ public List findOrderByCreatedDate(Integer generation, Part part, Status status, Pageable pageable) {
return queryFactory
.selectFrom(alarm)
.where(
@@ -37,7 +37,7 @@ public List getAlarms(Integer generation, Part part, Status status, Pagea
}
@Override
- public int countByGenerationAndPartAndStatus(int generation, Part part, Status status) {
+ public int count(int generation, Part part, Status status) {
return Math.toIntExact(queryFactory
.select(alarm.count())
.from(alarm)
diff --git a/src/main/java/org/sopt/makers/operation/entity/Attendance.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/Attendance.java
similarity index 62%
rename from src/main/java/org/sopt/makers/operation/entity/Attendance.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/Attendance.java
index 7539f0f4..8398cca1 100644
--- a/src/main/java/org/sopt/makers/operation/entity/Attendance.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/Attendance.java
@@ -1,27 +1,27 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.attendance.domain;
-import static javax.persistence.GenerationType.*;
-import static org.sopt.makers.operation.common.ExceptionMessage.*;
-import static org.sopt.makers.operation.entity.AttendanceStatus.*;
+import static jakarta.persistence.GenerationType.*;
+import static org.sopt.makers.operation.code.failure.AttendanceFailureCode.*;
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.EntityNotFoundException;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.OneToMany;
-
-import org.sopt.makers.operation.entity.lecture.Lecture;
-
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.member.domain.Member;
+import org.sopt.makers.operation.exception.AttendanceException;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
import lombok.*;
@Entity
@@ -29,7 +29,8 @@
@Getter
public class Attendance {
- @Id @GeneratedValue(strategy = IDENTITY)
+ @Id
+ @GeneratedValue(strategy = IDENTITY)
@Column(name = "attendance_id")
private Long id;
@@ -50,7 +51,23 @@ public class Attendance {
public Attendance(Member member, Lecture lecture) {
setMember(member);
setLecture(lecture);
- this.status = AttendanceStatus.ABSENT;
+ this.status = ABSENT;
+ }
+
+ private void setMember(Member member) {
+ if (Objects.nonNull(this.member)) {
+ this.member.getAttendances().remove(this);
+ }
+ this.member = member;
+ member.getAttendances().add(this);
+ }
+
+ private void setLecture(Lecture lecture) {
+ if (Objects.nonNull(this.lecture)) {
+ this.lecture.getAttendances().remove(this);
+ }
+ this.lecture = lecture;
+ lecture.getAttendances().add(this);
}
public void updateStatus() {
@@ -60,6 +77,7 @@ public void updateStatus() {
public AttendanceStatus getStatus() {
val first = getSubAttendanceByRound(1);
val second = getSubAttendanceByRound(2);
+
return switch (this.lecture.getAttribute()) {
case SEMINAR -> second.getStatus().equals(ATTENDANCE)
? first.getStatus().equals(ATTENDANCE) ? ATTENDANCE : TARDY
@@ -70,7 +88,9 @@ public AttendanceStatus getStatus() {
}
public float getScore() {
- return switch (this.lecture.getAttribute()) {
+ val lectureAttribute = this.lecture.getAttribute();
+
+ return switch (lectureAttribute) {
case SEMINAR -> {
if (this.status.equals(ABSENT)) {
yield -1f;
@@ -89,28 +109,18 @@ public void updateMemberScore() {
this.member.updateScore(this.getScore());
}
- public void revertMemberScore() {
+ public void restoreMemberScore() {
this.member.updateScore((-1) * this.getScore());
}
- private SubAttendance getSubAttendanceByRound(int round) {
- return this.subAttendances.stream().filter(o -> o.getSubLecture().getRound() == round).findFirst()
- .orElseThrow(() -> new EntityNotFoundException(INVALID_SUB_ATTENDANCE.getName()));
- }
-
- private void setMember(Member member) {
- if (Objects.nonNull(this.member)) {
- this.member.getAttendances().remove(this);
- }
- this.member = member;
- member.getAttendances().add(this);
+ public boolean isEnd() {
+ return this.lecture.isEnd();
}
- private void setLecture(Lecture lecture) {
- if (Objects.nonNull(this.lecture)) {
- this.lecture.getAttendances().remove(this);
- }
- this.lecture = lecture;
- lecture.getAttendances().add(this);
+ private SubAttendance getSubAttendanceByRound(int round) {
+ return this.subAttendances.stream()
+ .filter(o -> o.getSubLecture().getRound() == round)
+ .findFirst()
+ .orElseThrow(() -> new AttendanceException(INVALID_SUB_ATTENDANCE));
}
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/AttendanceStatus.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/AttendanceStatus.java
new file mode 100644
index 00000000..1ad72922
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/AttendanceStatus.java
@@ -0,0 +1,18 @@
+package org.sopt.makers.operation.attendance.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum AttendanceStatus {
+ ATTENDANCE("출석"),
+ ABSENT("결석"),
+ TARDY("지각"),
+ LEAVE_EARLY("조퇴"),
+ PARTICIPATE("참여"),
+ NOT_PARTICIPATE("미참여")
+ ;
+
+ final String name;
+}
diff --git a/src/main/java/org/sopt/makers/operation/entity/SubAttendance.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/SubAttendance.java
similarity index 60%
rename from src/main/java/org/sopt/makers/operation/entity/SubAttendance.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/SubAttendance.java
index 0099b00d..b9c0f667 100644
--- a/src/main/java/org/sopt/makers/operation/entity/SubAttendance.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/domain/SubAttendance.java
@@ -1,19 +1,22 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.attendance.domain;
-import static javax.persistence.GenerationType.*;
+import static org.sopt.makers.operation.attendance.domain.AttendanceStatus.*;
import java.util.Objects;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
+import org.sopt.makers.operation.common.domain.BaseEntity;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -22,7 +25,8 @@
@NoArgsConstructor
public class SubAttendance extends BaseEntity {
- @Id @GeneratedValue(strategy = IDENTITY)
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "sub_attendance_id")
private Long id;
@@ -40,7 +44,7 @@ public class SubAttendance extends BaseEntity {
public SubAttendance(Attendance attendance, SubLecture subLecture) {
setAttendance(attendance);
setSubLecture(subLecture);
- status = AttendanceStatus.ABSENT;
+ status = ABSENT;
}
private void setAttendance(Attendance attendance) {
@@ -63,4 +67,8 @@ public void updateStatus(AttendanceStatus status) {
this.status = status;
this.attendance.updateStatus();
}
+
+ public boolean isMatchRound(int round) {
+ return this.subLecture.getRound() == round;
+ }
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceCustomRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceCustomRepository.java
new file mode 100644
index 00000000..2dee226d
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceCustomRepository.java
@@ -0,0 +1,17 @@
+package org.sopt.makers.operation.attendance.repository.attendance;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.member.domain.Member;
+import org.springframework.data.domain.Pageable;
+
+public interface AttendanceCustomRepository {
+ List findAttendanceByMemberId(Long memberId);
+ List findFetchJoin(Lecture lecture, Part part, Pageable pageable);
+ List findFetchJoin(Member member);
+ List findToday(long memberPlaygroundId);
+ int count(Lecture lecture, Part part);
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepository.java
new file mode 100644
index 00000000..01bdf46e
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepository.java
@@ -0,0 +1,14 @@
+package org.sopt.makers.operation.attendance.repository.attendance;
+
+import java.util.Optional;
+
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.member.domain.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface AttendanceRepository extends JpaRepository, AttendanceCustomRepository {
+ Optional findByLectureAndMember(Lecture lecture, Member member);
+ void deleteByLecture(Lecture lecture);
+}
diff --git a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepositoryImpl.java
similarity index 70%
rename from src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepositoryImpl.java
index 708592c1..9020a32e 100644
--- a/src/main/java/org/sopt/makers/operation/repository/attendance/AttendanceRepositoryImpl.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/attendance/AttendanceRepositoryImpl.java
@@ -1,25 +1,25 @@
-package org.sopt.makers.operation.repository.attendance;
+package org.sopt.makers.operation.attendance.repository.attendance;
import static com.querydsl.core.types.dsl.Expressions.*;
import static java.util.Objects.*;
-import static org.sopt.makers.operation.entity.QAttendance.*;
-import static org.sopt.makers.operation.entity.QMember.*;
-import static org.sopt.makers.operation.entity.QSubAttendance.*;
-import static org.sopt.makers.operation.entity.QSubLecture.*;
-import static org.sopt.makers.operation.entity.lecture.QLecture.*;
+import static org.sopt.makers.operation.attendance.domain.QAttendance.*;
+import static org.sopt.makers.operation.attendance.domain.QSubAttendance.*;
+import static org.sopt.makers.operation.common.domain.Part.*;
+import static org.sopt.makers.operation.lecture.domain.QLecture.*;
+import static org.sopt.makers.operation.lecture.domain.QSubLecture.*;
+import static org.sopt.makers.operation.member.domain.QMember.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
-import lombok.val;
-
import org.sopt.makers.operation.config.ValueConfig;
-import org.sopt.makers.operation.entity.Attendance;
-import org.sopt.makers.operation.entity.Member;
-import org.sopt.makers.operation.entity.Part;
-import org.sopt.makers.operation.entity.lecture.LectureStatus;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.LectureStatus;
+import org.sopt.makers.operation.member.domain.Member;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
@@ -27,6 +27,7 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
+import lombok.val;
@Repository
@RequiredArgsConstructor
@@ -37,7 +38,6 @@ public class AttendanceRepositoryImpl implements AttendanceCustomRepository {
@Override
public List findAttendanceByMemberId(Long memberId) {
-
return queryFactory
.select(attendance)
.from(attendance)
@@ -51,15 +51,14 @@ public List findAttendanceByMemberId(Long memberId) {
}
@Override
- public List findByLecture(Long lectureId, Part part, Pageable pageable) {
+ public List findFetchJoin(Lecture lecture, Part part, Pageable pageable) {
return queryFactory
.selectFrom(attendance)
.leftJoin(attendance.subAttendances, subAttendance).fetchJoin()
.leftJoin(subAttendance.subLecture, subLecture).fetchJoin()
- .leftJoin(attendance.lecture, lecture).fetchJoin()
.leftJoin(attendance.member, member).fetchJoin()
.where(
- attendance.lecture.id.eq(lectureId),
+ attendance.lecture.eq(lecture),
partEq(part)
)
.orderBy(stringTemplate("SUBSTR({0}, 1, 1)", member.name).asc())
@@ -69,7 +68,7 @@ public List findByLecture(Long lectureId, Part part, Pageable pageab
}
@Override
- public List findByMember(Member member) {
+ public List findFetchJoin(Member member) {
return queryFactory
.selectFrom(attendance)
.leftJoin(attendance.subAttendances, subAttendance).fetchJoin().distinct()
@@ -94,26 +93,26 @@ public List findToday(long memberPlaygroundId) {
.where(
member.playgroundId.eq(memberPlaygroundId),
member.generation.eq(valueConfig.getGENERATION()),
- lecture.part.eq(member.part).or(lecture.part.eq(Part.ALL)),
+ lecture.part.eq(member.part).or(lecture.part.eq(ALL)),
lecture.startDate.between(startOfDay, endOfDay))
.orderBy(lecture.startDate.asc())
.fetch();
}
@Override
- public int countByLectureIdAndPart(long lectureId, Part part) {
+ public int count(Lecture lecture, Part part) {
return Math.toIntExact(queryFactory
.select(attendance.count())
.from(attendance)
.where(
- attendance.lecture.id.eq(lectureId),
- nonNull(part) ? attendance.member.part.eq(part) : null
+ attendance.lecture.eq(lecture),
+ partEq(part)
)
.fetchFirst()
);
}
private BooleanExpression partEq(Part part) {
- return nonNull(part) ? member.part.eq(part) : null;
+ return (isNull(part) || part.equals(ALL)) ? null : attendance.member.part.eq(part);
}
}
diff --git a/src/main/java/org/sopt/makers/operation/repository/SubAttendanceRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/subAttendance/SubAttendanceRepository.java
similarity index 71%
rename from src/main/java/org/sopt/makers/operation/repository/SubAttendanceRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/subAttendance/SubAttendanceRepository.java
index 8362cbfe..a3cf761f 100644
--- a/src/main/java/org/sopt/makers/operation/repository/SubAttendanceRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/attendance/repository/subAttendance/SubAttendanceRepository.java
@@ -1,9 +1,9 @@
-package org.sopt.makers.operation.repository;
+package org.sopt.makers.operation.attendance.repository.subAttendance;
import java.util.List;
-import org.sopt.makers.operation.entity.SubAttendance;
-import org.sopt.makers.operation.entity.SubLecture;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
diff --git a/src/main/java/org/sopt/makers/operation/config/JpaAuditingConfig.java b/operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaAuditingConfig.java
similarity index 80%
rename from src/main/java/org/sopt/makers/operation/config/JpaAuditingConfig.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaAuditingConfig.java
index b6561dd0..72be7d31 100644
--- a/src/main/java/org/sopt/makers/operation/config/JpaAuditingConfig.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaAuditingConfig.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.config;
+package org.sopt.makers.operation.common.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
diff --git a/src/main/java/org/sopt/makers/operation/config/JpaQueryFactoryConfig.java b/operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaQueryFactoryConfig.java
similarity index 77%
rename from src/main/java/org/sopt/makers/operation/config/JpaQueryFactoryConfig.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaQueryFactoryConfig.java
index 06d4078c..355cdb28 100644
--- a/src/main/java/org/sopt/makers/operation/config/JpaQueryFactoryConfig.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/common/config/JpaQueryFactoryConfig.java
@@ -1,12 +1,12 @@
-package org.sopt.makers.operation.config;
-
-import javax.persistence.EntityManager;
+package org.sopt.makers.operation.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+
@Configuration
public class JpaQueryFactoryConfig {
diff --git a/src/main/java/org/sopt/makers/operation/entity/BaseEntity.java b/operation-domain/src/main/java/org/sopt/makers/operation/common/domain/BaseEntity.java
similarity index 77%
rename from src/main/java/org/sopt/makers/operation/entity/BaseEntity.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/common/domain/BaseEntity.java
index b68b25db..ec685458 100644
--- a/src/main/java/org/sopt/makers/operation/entity/BaseEntity.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/common/domain/BaseEntity.java
@@ -1,14 +1,13 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.common.domain;
import java.time.LocalDateTime;
-import javax.persistence.EntityListeners;
-import javax.persistence.MappedSuperclass;
-
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
@EntityListeners(AuditingEntityListener.class)
diff --git a/src/main/java/org/sopt/makers/operation/entity/Part.java b/operation-domain/src/main/java/org/sopt/makers/operation/common/domain/Part.java
similarity index 84%
rename from src/main/java/org/sopt/makers/operation/entity/Part.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/common/domain/Part.java
index 6ccfe2e2..e39f073c 100644
--- a/src/main/java/org/sopt/makers/operation/entity/Part.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/common/domain/Part.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.common.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
diff --git a/src/main/java/org/sopt/makers/operation/entity/lecture/Attribute.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Attribute.java
similarity index 56%
rename from src/main/java/org/sopt/makers/operation/entity/lecture/Attribute.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Attribute.java
index b5869d72..1a46a21e 100644
--- a/src/main/java/org/sopt/makers/operation/entity/lecture/Attribute.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Attribute.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity.lecture;
+package org.sopt.makers.operation.lecture.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -6,7 +6,10 @@
@Getter
@AllArgsConstructor
public enum Attribute {
- SEMINAR("세미나"), EVENT("행사"), ETC("기타");
+ SEMINAR("세미나"),
+ EVENT("행사"),
+ ETC("기타"),
+ ;
private final String name;
}
diff --git a/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Lecture.java
similarity index 61%
rename from src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Lecture.java
index 8fd2b9af..7d412863 100644
--- a/src/main/java/org/sopt/makers/operation/entity/lecture/Lecture.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/Lecture.java
@@ -1,25 +1,23 @@
-package org.sopt.makers.operation.entity.lecture;
+package org.sopt.makers.operation.lecture.domain;
-import static javax.persistence.GenerationType.*;
-import static org.sopt.makers.operation.entity.lecture.LectureStatus.*;
+import static org.sopt.makers.operation.lecture.domain.LectureStatus.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.OneToMany;
-
-import org.sopt.makers.operation.entity.Attendance;
-import org.sopt.makers.operation.entity.BaseEntity;
-import org.sopt.makers.operation.entity.Part;
-import org.sopt.makers.operation.entity.SubLecture;
-
+import org.sopt.makers.operation.common.domain.BaseEntity;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.attendance.domain.Attendance;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -29,7 +27,8 @@
@Getter
public class Lecture extends BaseEntity {
- @Id @GeneratedValue(strategy = IDENTITY)
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "lecture_id")
private Long id;
@@ -59,8 +58,15 @@ public class Lecture extends BaseEntity {
List attendances = new ArrayList<>();
@Builder
- public Lecture(String name, Part part, int generation, String place, LocalDateTime startDate, LocalDateTime endDate,
- Attribute attribute) {
+ public Lecture(
+ String name,
+ Part part,
+ int generation,
+ String place,
+ LocalDateTime startDate,
+ LocalDateTime endDate,
+ Attribute attribute
+ ) {
this.name = name;
this.part = part;
this.generation = generation;
@@ -91,4 +97,8 @@ public boolean isBefore() {
public boolean isFirst() {
return this.lectureStatus.equals(FIRST);
}
+
+ public boolean isNotYetToEnd() {
+ return this.endDate.isAfter(LocalDateTime.now());
+ }
}
diff --git a/src/main/java/org/sopt/makers/operation/entity/lecture/LectureStatus.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/LectureStatus.java
similarity index 54%
rename from src/main/java/org/sopt/makers/operation/entity/lecture/LectureStatus.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/LectureStatus.java
index a121b08f..246827ff 100644
--- a/src/main/java/org/sopt/makers/operation/entity/lecture/LectureStatus.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/LectureStatus.java
@@ -1,4 +1,4 @@
-package org.sopt.makers.operation.entity.lecture;
+package org.sopt.makers.operation.lecture.domain;
public enum LectureStatus {
BEFORE, FIRST, SECOND, END
diff --git a/src/main/java/org/sopt/makers/operation/entity/SubLecture.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/SubLecture.java
similarity index 52%
rename from src/main/java/org/sopt/makers/operation/entity/SubLecture.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/SubLecture.java
index b38dfca7..47b85b0f 100644
--- a/src/main/java/org/sopt/makers/operation/entity/SubLecture.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/domain/SubLecture.java
@@ -1,25 +1,23 @@
-package org.sopt.makers.operation.entity;
+package org.sopt.makers.operation.lecture.domain;
-import static javax.persistence.GenerationType.*;
-import static org.sopt.makers.operation.entity.lecture.LectureStatus.*;
+import static java.util.Objects.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.OneToMany;
-
-import org.sopt.makers.operation.entity.lecture.Lecture;
-import org.sopt.makers.operation.entity.lecture.LectureStatus;
+import org.sopt.makers.operation.attendance.domain.SubAttendance;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -28,7 +26,8 @@
@Getter
public class SubLecture {
- @Id @GeneratedValue(strategy = IDENTITY)
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "sub_lecture_id")
private Long id;
@@ -49,25 +48,37 @@ public SubLecture(Lecture lecture, int round) {
this.round = round;
}
- public void startAttendance(String code) {
- this.startAt = LocalDateTime.now();
+ private void setLecture(Lecture lecture) {
+ if (Objects.nonNull(this.lecture)) {
+ this.lecture.getSubLectures().remove(this);
+ }
+ this.lecture = lecture;
+ lecture.getSubLectures().add(this);
+ }
+
+ public void updateCode(String code) {
this.code = code;
+ this.startAt = LocalDateTime.now();
this.lecture.updateStatus(getUpdatedStatus());
}
private LectureStatus getUpdatedStatus() {
return switch (this.round) {
- case 1 -> FIRST;
- case 2 -> SECOND;
+ case 1 -> LectureStatus.FIRST;
+ case 2 -> LectureStatus.SECOND;
default -> this.lecture.getLectureStatus();
};
}
- private void setLecture(Lecture lecture) {
- if (Objects.nonNull(this.lecture)) {
- this.lecture.getSubLectures().remove(this);
- }
- this.lecture = lecture;
- lecture.getSubLectures().add(this);
+ public boolean isNotStarted() {
+ return isNull(this.startAt) || isNull(this.code) || this.startAt.isAfter(LocalDateTime.now());
+ }
+
+ public boolean isEnded(int attendanceMinute) {
+ return this.startAt.plusMinutes(attendanceMinute).isBefore(LocalDateTime.now());
+ }
+
+ public boolean isMatchCode(String code) {
+ return this.code.equals(code);
}
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureCustomRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureCustomRepository.java
new file mode 100644
index 00000000..738e1679
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureCustomRepository.java
@@ -0,0 +1,12 @@
+package org.sopt.makers.operation.lecture.repository.lecture;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+
+public interface LectureCustomRepository {
+ List find(int generation, Part part);
+ List findLecturesReadyToEnd();
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepository.java
new file mode 100644
index 00000000..210f9f7a
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepository.java
@@ -0,0 +1,7 @@
+package org.sopt.makers.operation.lecture.repository.lecture;
+
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface LectureRepository extends JpaRepository, LectureCustomRepository {
+}
diff --git a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepositoryImpl.java
similarity index 59%
rename from src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepositoryImpl.java
index 51960170..281831ae 100644
--- a/src/main/java/org/sopt/makers/operation/repository/lecture/LectureRepositoryImpl.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/lecture/LectureRepositoryImpl.java
@@ -1,21 +1,22 @@
-package org.sopt.makers.operation.repository.lecture;
+package org.sopt.makers.operation.lecture.repository.lecture;
-import com.querydsl.core.types.dsl.BooleanExpression;
-import com.querydsl.jpa.impl.JPAQueryFactory;
-import lombok.RequiredArgsConstructor;
-import org.sopt.makers.operation.entity.Part;
-import org.sopt.makers.operation.entity.lecture.Lecture;
-import org.springframework.stereotype.Repository;
+import static java.util.Objects.*;
+import static org.sopt.makers.operation.attendance.domain.QAttendance.*;
+import static org.sopt.makers.operation.lecture.domain.QLecture.*;
+import static org.sopt.makers.operation.member.domain.QMember.*;
import java.time.LocalDateTime;
import java.util.List;
-import java.util.Optional;
-import static java.util.Objects.*;
-import static org.sopt.makers.operation.entity.QAttendance.*;
-import static org.sopt.makers.operation.entity.QMember.*;
-import static org.sopt.makers.operation.entity.lecture.LectureStatus.*;
-import static org.sopt.makers.operation.entity.lecture.QLecture.*;
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.LectureStatus;
+import org.springframework.stereotype.Repository;
+
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
@@ -35,28 +36,18 @@ public List find(int generation, Part part) {
}
@Override
- public List findLecturesToBeEnd() {
+ public List findLecturesReadyToEnd() {
return queryFactory
.selectFrom(lecture)
.leftJoin(lecture.attendances, attendance).fetchJoin().distinct()
.leftJoin(attendance.member, member).fetchJoin().distinct()
.where(
lecture.endDate.before(LocalDateTime.now()),
- lecture.lectureStatus.ne(END)
+ lecture.lectureStatus.ne(LectureStatus.END)
)
.fetch();
}
- @Override
- public Optional find(Long lectureId) {
- return queryFactory
- .selectFrom(lecture)
- .leftJoin(lecture.attendances, attendance).fetchJoin().distinct()
- .leftJoin(attendance.member, member).fetchJoin().distinct()
- .where(lecture.id.eq(lectureId))
- .stream().findFirst();
- }
-
private BooleanExpression partEq(Part part) {
return nonNull(part) ? lecture.part.eq(part) : null;
}
diff --git a/src/main/java/org/sopt/makers/operation/repository/lecture/SubLectureRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/subLecture/SubLectureRepository.java
similarity index 64%
rename from src/main/java/org/sopt/makers/operation/repository/lecture/SubLectureRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/subLecture/SubLectureRepository.java
index 4eeb7d85..f34934ea 100644
--- a/src/main/java/org/sopt/makers/operation/repository/lecture/SubLectureRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/lecture/repository/subLecture/SubLectureRepository.java
@@ -1,16 +1,12 @@
-package org.sopt.makers.operation.repository.lecture;
+package org.sopt.makers.operation.lecture.repository.subLecture;
-import org.sopt.makers.operation.entity.SubLecture;
-import org.sopt.makers.operation.entity.lecture.Lecture;
+import org.sopt.makers.operation.lecture.domain.Lecture;
+import org.sopt.makers.operation.lecture.domain.SubLecture;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
-import java.util.Optional;
-
public interface SubLectureRepository extends JpaRepository {
- Optional findById(Long subLectureId);
-
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("delete from SubLecture sl where sl.lecture = :lecture")
void deleteAllByLecture(Lecture lecture);
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/Member.java b/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/Member.java
new file mode 100644
index 00000000..caafb2c5
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/Member.java
@@ -0,0 +1,62 @@
+package org.sopt.makers.operation.member.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.sopt.makers.operation.attendance.domain.Attendance;
+import org.sopt.makers.operation.common.domain.Part;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor
+@Getter
+public class Member {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "member_id")
+ private Long id;
+
+ private Long playgroundId;
+
+ private String name;
+ private int generation;
+
+ @Enumerated(EnumType.STRING)
+ private ObYb obyb;
+
+ @Enumerated(EnumType.STRING)
+ private Part part;
+
+ private String university;
+ private float score;
+ private String phone;
+
+ @OneToMany(mappedBy = "member")
+ List attendances = new ArrayList<>();
+
+ public void updateScore(float score) {
+ this.score += score;
+ }
+
+ public void updateTotalScore() {
+ this.score = calcAllAttendances();
+ }
+
+ private float calcAllAttendances() {
+ return (float) (2 + this.attendances.stream()
+ .filter(Attendance::isEnd)
+ .mapToDouble(Attendance::getScore)
+ .sum());
+ }
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/ObYb.java b/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/ObYb.java
new file mode 100644
index 00000000..672c84e6
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/member/domain/ObYb.java
@@ -0,0 +1,5 @@
+package org.sopt.makers.operation.member.domain;
+
+public enum ObYb {
+ OB, YB
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberCustomRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberCustomRepository.java
new file mode 100644
index 00000000..93493bbe
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberCustomRepository.java
@@ -0,0 +1,13 @@
+package org.sopt.makers.operation.member.repository;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.member.domain.Member;
+import org.springframework.data.domain.Pageable;
+
+public interface MemberCustomRepository {
+ int count(int generation, Part part);
+ List find(int generation, Part part);
+ List find(int generation, Part part, Pageable pageable);
+}
diff --git a/src/main/java/org/sopt/makers/operation/repository/member/MemberRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepository.java
similarity index 65%
rename from src/main/java/org/sopt/makers/operation/repository/member/MemberRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepository.java
index ec0c0cbe..45184292 100644
--- a/src/main/java/org/sopt/makers/operation/repository/member/MemberRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepository.java
@@ -1,11 +1,10 @@
-package org.sopt.makers.operation.repository.member;
-
-import org.sopt.makers.operation.entity.Member;
-import org.springframework.data.jpa.repository.JpaRepository;
+package org.sopt.makers.operation.member.repository;
import java.util.Optional;
+import org.sopt.makers.operation.member.domain.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+
public interface MemberRepository extends JpaRepository, MemberCustomRepository {
Optional getMemberByPlaygroundIdAndGeneration(Long id, int generation);
- boolean existsByPlaygroundId(Long id);
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepositoryImpl.java b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepositoryImpl.java
new file mode 100644
index 00000000..4f4a17f8
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/member/repository/MemberRepositoryImpl.java
@@ -0,0 +1,67 @@
+package org.sopt.makers.operation.member.repository;
+
+import static java.util.Objects.*;
+import static org.sopt.makers.operation.common.domain.Part.*;
+import static org.sopt.makers.operation.member.domain.QMember.*;
+
+import java.util.List;
+
+import org.sopt.makers.operation.common.domain.Part;
+import org.sopt.makers.operation.member.domain.Member;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.Expressions;
+import com.querydsl.core.types.dsl.StringExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import lombok.RequiredArgsConstructor;
+
+@Repository
+@RequiredArgsConstructor
+public class MemberRepositoryImpl implements MemberCustomRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public int count(int generation, Part part) {
+ return Math.toIntExact(queryFactory
+ .select(member.count())
+ .from(member)
+ .where(
+ member.generation.eq(generation),
+ partEq(part)
+ )
+ .fetchFirst());
+ }
+
+ @Override
+ public List find(int generation, Part part) {
+ return queryFactory
+ .selectFrom(member)
+ .where(
+ member.generation.eq(generation),
+ partEq(part))
+ .fetch();
+ }
+
+ private BooleanExpression partEq(Part part) {
+ return (isNull(part) || part.equals(ALL)) ? null : member.part.eq(part);
+ }
+
+ @Override
+ public List find(int generation, Part part, Pageable pageable) {
+ StringExpression firstName = Expressions.stringTemplate("SUBSTR({0}, 1, 1)", member.name);
+ return queryFactory
+ .selectFrom(member)
+ .where(
+ partEq(part),
+ member.generation.eq(generation)
+ )
+ .offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .orderBy(firstName.asc())
+ .fetch();
+ }
+}
diff --git a/src/main/java/org/sopt/makers/operation/converter/StringListConverter.java b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/converter/StringListConverter.java
similarity index 83%
rename from src/main/java/org/sopt/makers/operation/converter/StringListConverter.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/schedule/converter/StringListConverter.java
index fe244329..4f0fdc2d 100644
--- a/src/main/java/org/sopt/makers/operation/converter/StringListConverter.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/converter/StringListConverter.java
@@ -1,15 +1,14 @@
-package org.sopt.makers.operation.converter;
+package org.sopt.makers.operation.schedule.converter;
import static com.fasterxml.jackson.databind.DeserializationFeature.*;
-import java.util.List;
import java.io.IOException;
+import java.util.List;
-import javax.persistence.AttributeConverter;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.persistence.AttributeConverter;
public class StringListConverter implements AttributeConverter, String> {
private static final ObjectMapper mapper = new ObjectMapper()
@@ -20,7 +19,7 @@ public class StringListConverter implements AttributeConverter, Str
public String convertToDatabaseColumn(List attribute) {
try {
return mapper.writeValueAsString(attribute);
- } catch (JsonProcessingException ex) {
+ } catch (IOException ex) {
throw new IllegalArgumentException(ex.getMessage());
}
}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/schedule/domain/Schedule.java b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/domain/Schedule.java
new file mode 100644
index 00000000..6852ef09
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/domain/Schedule.java
@@ -0,0 +1,39 @@
+package org.sopt.makers.operation.schedule.domain;
+
+import java.time.LocalDateTime;
+
+import org.sopt.makers.operation.common.domain.BaseEntity;
+import org.sopt.makers.operation.lecture.domain.Attribute;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@NoArgsConstructor
+@Getter
+public class Schedule extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "schedule_id")
+ private Long id;
+
+ private LocalDateTime startDate;
+
+ private LocalDateTime endDate;
+
+ @Column(nullable = false)
+ @Enumerated(value = EnumType.STRING)
+ private Attribute attribute;
+
+ private String title;
+
+ private String location;
+}
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleCustomRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleCustomRepository.java
new file mode 100644
index 00000000..ce77b40d
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleCustomRepository.java
@@ -0,0 +1,10 @@
+package org.sopt.makers.operation.schedule.repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.sopt.makers.operation.schedule.domain.Schedule;
+
+public interface ScheduleCustomRepository {
+ List findBetween(LocalDateTime startDate, LocalDateTime endDate);
+}
diff --git a/src/main/java/org/sopt/makers/operation/repository/schedule/ScheduleRepository.java b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepository.java
similarity index 60%
rename from src/main/java/org/sopt/makers/operation/repository/schedule/ScheduleRepository.java
rename to operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepository.java
index c651e850..2226c637 100644
--- a/src/main/java/org/sopt/makers/operation/repository/schedule/ScheduleRepository.java
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepository.java
@@ -1,6 +1,6 @@
-package org.sopt.makers.operation.repository.schedule;
+package org.sopt.makers.operation.schedule.repository;
-import org.sopt.makers.operation.entity.Member;
+import org.sopt.makers.operation.member.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ScheduleRepository extends JpaRepository, ScheduleCustomRepository {
diff --git a/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepositoryImpl.java b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepositoryImpl.java
new file mode 100644
index 00000000..92090bab
--- /dev/null
+++ b/operation-domain/src/main/java/org/sopt/makers/operation/schedule/repository/ScheduleRepositoryImpl.java
@@ -0,0 +1,37 @@
+package org.sopt.makers.operation.schedule.repository;
+
+import static org.sopt.makers.operation.schedule.domain.QSchedule.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.sopt.makers.operation.schedule.domain.Schedule;
+import org.springframework.stereotype.Repository;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+
+import lombok.RequiredArgsConstructor;
+
+@Repository
+@RequiredArgsConstructor
+public class ScheduleRepositoryImpl implements ScheduleCustomRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public List findBetween(LocalDateTime startDate, LocalDateTime endDate) {
+ return queryFactory
+ .select(schedule)
+ .from(schedule)
+ .where(
+ schedule.startDate.eq(startDate)
+ .or(schedule.startDate.between(startDate, endDate))
+ .or(schedule.startDate.eq(endDate))
+ .or(schedule.endDate.eq(startDate))
+ .or(schedule.endDate.between(startDate, endDate))
+ .or(schedule.endDate.eq(endDate))
+ )
+ .orderBy(schedule.startDate.asc())
+ .fetch();
+ }
+}
diff --git a/operation-external/build.gradle b/operation-external/build.gradle
new file mode 100644
index 00000000..4c29cb51
--- /dev/null
+++ b/operation-external/build.gradle
@@ -0,0 +1,18 @@
+jar {
+ enabled = true
+}
+
+bootJar {
+ enabled = false
+}
+
+dependencies {
+ implementation project(path: ':operation-common')
+ implementation project(path: ':operation-domain')
+
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/operation-external/src/main/java/org/sopt/makers/operation/ExternalRoot.java b/operation-external/src/main/java/org/sopt/makers/operation/ExternalRoot.java
new file mode 100644
index 00000000..a3168705
--- /dev/null
+++ b/operation-external/src/main/java/org/sopt/makers/operation/ExternalRoot.java
@@ -0,0 +1,4 @@
+package org.sopt.makers.operation;
+
+public interface ExternalRoot {
+}
diff --git a/operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSender.java b/operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSender.java
new file mode 100644
index 00000000..60e39472
--- /dev/null
+++ b/operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSender.java
@@ -0,0 +1,7 @@
+package org.sopt.makers.operation.client.alarm;
+
+import org.sopt.makers.operation.client.alarm.dto.AlarmSenderRequest;
+
+public interface AlarmSender {
+ void send(AlarmSenderRequest request);
+}
diff --git a/src/main/java/org/sopt/makers/operation/external/api/AlarmSenderImpl.java b/operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSenderImpl.java
similarity index 51%
rename from src/main/java/org/sopt/makers/operation/external/api/AlarmSenderImpl.java
rename to operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSenderImpl.java
index d6bf50ff..8f3e59f4 100644
--- a/src/main/java/org/sopt/makers/operation/external/api/AlarmSenderImpl.java
+++ b/operation-external/src/main/java/org/sopt/makers/operation/client/alarm/AlarmSenderImpl.java
@@ -1,20 +1,17 @@
-package org.sopt.makers.operation.external.api;
+package org.sopt.makers.operation.client.alarm;
import static java.util.Objects.*;
import static java.util.UUID.*;
-import static org.sopt.makers.operation.common.ExceptionMessage.*;
+import static org.sopt.makers.operation.code.failure.AlarmFailureCode.*;
import static org.springframework.http.MediaType.*;
-import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import org.sopt.makers.operation.dto.alarm.AlarmSendResponseDTO;
-import org.sopt.makers.operation.dto.alarm.AlarmSenderDTO;
+import org.sopt.makers.operation.client.alarm.dto.AlarmSenderRequest;
+import org.sopt.makers.operation.config.ValueConfig;
import org.sopt.makers.operation.exception.AlarmException;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
@@ -27,59 +24,48 @@
@Component
@RequiredArgsConstructor
public class AlarmSenderImpl implements AlarmSender {
- private final RestTemplate restTemplate;
- @Value("${notification.key}")
- private String key;
- @Value("${notification.url}")
- private String host;
-
- private final List appLinkList = Arrays.asList(
- "home",
- "home/notification",
- "home/mypage",
- "home/attendance",
- "home/attendance/attendance-modal",
- "home/soptamp",
- "home/soptamp/entire-ranking",
- "home/soptamp/current-generation-ranking"
- );
- private final List webLinkList = Arrays.asList(
- "https://playground.sopt.org/members",
- "https://playground.sopt.org/group"
- );
+ private final RestTemplate restTemplate;
+ private final ValueConfig valueConfig;
@Override
- public void send(AlarmSenderDTO alarmSenderDTO) {
- val alarmRequest = getAlarmRequest(alarmSenderDTO);
- val headers = getHeaders();
- val entity = new HttpEntity<>(alarmRequest, headers);
-
+ public void send(AlarmSenderRequest request) {
try {
- restTemplate.postForEntity(host, entity, AlarmSendResponseDTO.class);
+ val host = valueConfig.getNOTIFICATION_URL();
+ val entity = getEntity(request);
+ restTemplate.postForEntity(host, entity, AlarmSenderRequest.class);
} catch (HttpClientErrorException e) {
- throw new AlarmException(FAIL_SEND_ALARM.getName());
+ throw new AlarmException(FAIL_SEND_ALARM);
}
}
- private Map