Compare commits

..

126 Commits
main ... main

Author SHA1 Message Date
82d8afcfb7 [] Class 관련 로직 추가
1. 컨트롤러, 서비스(인터페이스), 레포지토리(인터페이스) 추가
2. scoped 등록
3. 클래스 관련 모델 등록
4. ClassInfo API 등록
2025-06-20 17:59:03 +09:00
6ef2a32d24 [🐛] user/academy API EF 로직 수정 2025-06-19 17:28:08 +09:00
4e7130ea63 [🐛] 헤더 키 이름 변경 2025-06-18 13:22:13 +09:00
39359eea7a [🐛] http 설정 변경 2025-06-18 13:01:55 +09:00
6c8c9972b2 [🐛] 헤더 관련 로직 수정
1. 순서 변경
2. 주석 정리
2025-06-18 12:54:02 +09:00
2766b3489d [🐛] 로컬 설정 제거 2025-06-18 10:39:34 +09:00
e1aaf91a3b [🐛] 리디렉션 주소 수정 2025-06-17 16:59:27 +09:00
c15813804d [📝] 로컬 변경 2025-06-17 16:39:55 +09:00
9f6a5b882c [] 운영체제별 동작 로직 통일화
운영체제가 다르다고 다른 API 나 다른 로직을 타는게 아닌 하나의 로직으로 돌게 만들기 위해서 로직 수정
2025-06-16 17:44:42 +09:00
5baa549695 [] 유저 정보 전송 API 반환 정보 변경 2025-06-13 17:54:43 +09:00
669f22d365 [] 로그인 동작 및 Repository 동작 변경
1. 로그인 되었을 경우 화면 표기 변경
1.1. 사용자 이름 보내주기
1.2. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경
2025-06-11 15:14:42 +09:00
a9462ca9b5 [] 로그인 관련 데이터 로직 변경 2025-06-09 17:46:49 +09:00
6a543945e7 [🐛] kakao auth redirectURL 수정 2025-06-05 17:47:43 +09:00
80197c7775 [🐛] Build - Debug 에러 수정 2025-06-05 17:36:37 +09:00
1738878093 [] 세션 관리 API 추가
1. 웹 - 서버 데이터 관리 위한 세션 관리 API 추가
2025-06-04 17:53:02 +09:00
3ebb7137c0 [] 카카오 로그인
1. 카카오 로그인 인증 snsID 받아오기
2. 리다이렉트 동작 수정
3. 세션을 통한 토큰 저장
3.1. 세션서비스 생성
4. 회원가입 화면으로 이동
2025-05-30 17:50:06 +09:00
65962c01c2 [] 카카오 로그인 구현
1. 인가코드 받기
2. 리다이렉트 -> 인가 코드를 엑세스 토큰으로
3. User.Me -> snsID 받아오기
4. DB 에서 snsID 조회 까지 완료
2025-05-29 17:53:50 +09:00
bc4658afca [🐛] 서버 시점 변경 2025-05-28 15:55:02 +09:00
bb4de76af0 [🐛] 추가 안한 Scope 코드 제거 2025-05-28 15:47:39 +09:00
ff108ac299 [📝] Web 접속 헤더 설정 2025-05-28 15:34:13 +09:00
c58092c048 [] 쿠키 적용 및 작동 로직 수정 2025-05-26 17:43:05 +09:00
0eaf1eae79 [♻️] Jenkins 동작 로직 수정 2025-05-22 10:25:15 +09:00
197e2007ce [♻️] 개발 FRONT 테스트 2025-05-20 09:56:21 +09:00
6580ac27b8 [♻️] 개발 FRONT 화면 테스트 3 2025-05-20 09:47:38 +09:00
4ef3f3dc23 [♻️] 개발기 Front 검증 2 2025-05-19 17:00:19 +09:00
8f01f57c91 [♻️] 개발기 FRONT 검증 2025-05-19 16:40:26 +09:00
28ad68bfe6 Merge pull request '프론트 테스트 머지' (#2) from Academy into main
Reviewed-on: https://git.ipstein.myds.me/seonkyu.kim/AcaMate_API/pulls/2
2025-05-19 06:46:25 +00:00
d9166469f7 [] Front 빌드 테스트 2025-05-19 15:43:28 +09:00
5ab4940b5f [📝] gitignore 수정 2025-05-19 15:39:40 +09:00
3d185179e2 [📝] 25.04.14 TO-DO 기록 2025-04-14 09:41:11 +09:00
3b71565733 [🐛] 빈 값 리턴시 객체 리턴이 아닌 단순 string 리턴인 내용을 객체로 수정 2025-04-11 17:46:25 +09:00
5d79cde27f [] SignalR 기능 활용한 채팅 기능 추가 중 2 2025-04-10 17:55:16 +09:00
2b37dc2746 [] SignalR 기능 활용한 채팅 기능 추가 중 2025-04-09 17:56:02 +09:00
7f9811e1a3 Merge pull request 'refactory' (#1) from refactory into main
Reviewed-on: https://git.ipstein.myds.me/seonkyu.kim/AcaMate_API/pulls/1
2025-04-09 04:04:37 +00:00
306470ac75 [♻️] 전체적인 구조 리팩토링 진행완료 (Exception part) 2025-04-09 12:55:59 +09:00
04f94ca835 [♻️] 전체적인 구조 리팩토링 진행중 4 (PushController part) - 푸시까지 수정 완료 2025-04-08 17:17:12 +09:00
c1ee151a9c [♻️] 전체적인 구조 리팩토링 진행중3 (PushController part) - 오류 나서 빌드 안되는 중 2025-04-07 17:57:47 +09:00
0eb58a1edf [♻️] 전체적인 구조 리팩토링 진행중2 (PushController part) 2025-04-04 17:56:16 +09:00
7f0121a175 [♻️] 전체적인 구조 리팩토링 진행중2 (AppController part) 2025-04-04 16:22:44 +09:00
b0606a44bb [♻️] 전체적인 구조 리팩토링 진행중 2025-04-03 17:53:54 +09:00
d7a3703e29 [📝] 25.04.03 TO-DO 기록 2025-04-03 10:38:14 +09:00
ff714ed09f [] Chatting 관련 Db 설정 추가 2025-04-02 14:17:05 +09:00
9898b8364d [] Member 관련 API 생성 준비 2025-04-01 17:38:37 +09:00
a3bf37746d [🐛️] USER 회원가입 버그 발견 및 수정 2025-04-01 17:38:13 +09:00
5994c9dc2f Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Program/V1/Controllers/AppController.cs
2025-03-28 14:12:47 +09:00
fc428d5c85 [🐛] 헤더 디비 검증 로직 오류 수정 2025-03-28 14:07:28 +09:00
719c19e1e5 [🐛] 스웨거 인식 문제 오류 잡기
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-03-21 22:32:04 +09:00
7fc316520c [♻️] 헤더 예외 처리 수정 2025-03-20 16:47:12 +09:00
b2eec9c5a9 [📝] 25.03.20 TO-DO 기록 2025-03-20 14:21:22 +09:00
3c70f767a1 [] API Header 키 생성하는 로직 추가 중 2 2025-03-20 14:21:03 +09:00
8593620059 [📝] 25.03.20 TO-DO 기록 2025-03-20 10:31:49 +09:00
3b9954f4eb [📝] README 수정 2025-03-19 17:55:01 +09:00
992ec0fa77 [📝] 25.03.19 TO-DO 기록 2025-03-19 17:54:36 +09:00
079bb4bc29 [] API Header 키 생성하는 로직 추가 중 - 로그 저장 안되는 문제 2025-03-19 17:50:55 +09:00
6914bba007 [] API Header 점검하는 로직 추가 중 3 2025-03-19 12:40:05 +09:00
090e03c6b3 [📝] 25.03.19 TO-DO 기록 2025-03-19 12:39:10 +09:00
643708627a [] API Header 점검하는 로직 추가 중2 2025-03-18 18:00:22 +09:00
1f8ac2cff7 [♻️] 로그 종류 수정 2025-03-18 17:54:19 +09:00
50f740aa4b [] API Header 점검하는 로직 추가 중 2025-03-18 17:54:04 +09:00
cb159397af [♻️] 반환 코드 수정 2025-03-17 17:59:52 +09:00
0b65855e15 [♻️] JWT 토큰 관련해서 인증 로직 전제적으로 변경 2025-03-14 17:34:18 +09:00
96d8999317 [] 서버 접근을 위한 인증 API 생성 2025-03-13 15:47:51 +09:00
3e3a644203 [📝] 25.03.11 TO-DO 기록 2025-03-11 13:41:02 +09:00
2321980ee8 [] 회원 탈퇴 API 생성 2025-03-11 13:40:50 +09:00
6963c5eadb [] 회원 탈퇴 API 생성 2025-03-11 13:40:37 +09:00
19fb34bc32 [♻️] USER API 로직 점검 및 로그 추가 2025-03-11 11:14:51 +09:00
d537ad7a25 [♻️] 푸시 API 예외 처리 수정 2025-03-11 09:22:25 +09:00
166fe3dd5d [📝] 25.03.11 TO-DO 기록 2025-03-11 08:59:52 +09:00
a0beed0415 [♻️] 푸시 API 내의 변수 이름 통일화 2025-03-10 17:05:09 +09:00
e6672ed630 [] 푸시 리스트 확인 및 푸시 리스트내 아이템 삭제, 그외 로직 변경 2025-03-10 16:48:29 +09:00
012e7231db [👷] 로그 정리 및 swagger 설명 정리 2025-03-10 15:53:09 +09:00
9acda11556 [♻️] 푸시 데이터 저장 로직 변경으로 인한 PUSH API 변경: send 2025-03-10 15:50:41 +09:00
af0d5e21c1 [📝] 25.03.10 TO-DO 변경 2025-03-10 15:07:18 +09:00
d8c16fee29 [📝] 25.03.10 TO-DO 기록 2025-03-10 09:44:32 +09:00
aec03b9df7 [] PUSH 삭제 API 작성 2025-03-07 17:33:00 +09:00
968bd33b9d [♻️] DB 저장 및 삭제 로직 변경 후 적용 2025-03-07 17:31:51 +09:00
f2df6cb1e6 [📝] 25.03.07 TO-DO 기록 2025-03-07 17:30:53 +09:00
07900147c6 [📝] 25.03.07 TO-DO 기록 2025-03-07 11:22:59 +09:00
9e004cb265 [📝] 25.03.06 TO-DO 완료 내역 저장 2025-03-06 17:54:19 +09:00
ccd880e7e0 [] PUSH 확인, DELETE API 작성, 로그 기능 구현 위한 로직 추가 중 2025-03-06 17:53:36 +09:00
8cb4207ef6 [♻️] 푸시 로직을 아카데미와 연계시켜 bid를 사용하게 변경 2025-03-06 13:42:49 +09:00
289fe748a9 [📝] 25.03.06 TO-DO 기록 2025-03-06 13:41:53 +09:00
ecddaa2575 [] 푸시 내용 변경 API 생성 2025-03-05 17:26:39 +09:00
76d989a4fa [♻️] PUSH 로직 변경 - uid 와 pid 를 활용해 푸시 전송가능, 뱃지 설정 가능, 컨텐츠 추가 가능 2025-03-05 15:30:06 +09:00
f443f3410c [♻️] PUSH API 리팩토링 진행 중_5 2025-02-27 17:25:26 +09:00
9413dbbea8 [♻️] PUSH API 리팩토링 진행 중_4 : 대용량 발신 위한 버전 작성 중 2025-02-27 17:17:42 +09:00
242f1a48df [♻️] PUSH API 리팩토링 진행 중_3 : 대용량 발신 위한 버전 작성 중 2025-02-27 17:09:35 +09:00
59fa5bd014 [♻️] PUSH API 리팩토링 진행 중_2 2025-02-27 15:44:15 +09:00
29072037a0 [♻️] PUSH API 리팩토링 진행 중 2025-02-27 15:20:50 +09:00
e8a2f3d7ee [♻️] User 파트와 Push 파트 리팩토링 진행 2025-02-27 12:56:34 +09:00
e20ac8bdbc [] 사용자 API 로직 구현 (로그인, 로그아웃, 회원가입) 2025-02-26 16:21:19 +09:00
47f3b30010 [♻️] APIResponse 리팩토링, Exception 세분화 진행 2025-02-26 11:20:08 +09:00
806541e4d7 [📝] gitignore 수정 2025-02-26 09:11:24 +09:00
41c7e3a814 [👷🏻] 깃 수정 중
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-26 00:32:16 +09:00
56ea832b8c [👷🏻] gitignore 수정 2025-02-25 22:54:05 +09:00
b5021eaaa4 [👷] DEV 서버 올리기 위한 수정 작업 2025-02-25 17:38:30 +09:00
0e2452207c [] JWT 인증 방식 도입, 로그인, 아카데미 API 에 JWT 인증 도입해서 로직 변경 2025-02-25 17:27:23 +09:00
f1a901820f [] JWT 생성 + 확인 로직 구현 중 & 회원가입, 로그아웃 API 구현 중 2025-02-24 17:54:38 +09:00
55f40e56cf [] 회원가입 시 회원 정보 저장 API & 토큰 추가 API 만드는 중 2025-02-21 17:53:32 +09:00
cebf5a42cb [[] 로그인 관련(회원가입, 회원조회) 부분 API 작업중 2025-02-20 17:47:51 +09:00
b2e72a2f9c [] 유저 데이터 불러오기 API 추가 + 아카데미 이름 불러오기 API 추가 2025-02-19 17:22:50 +09:00
Seonkyu_Kim
41c8d94001 [] 아카데미 정보 API 기능 추가 2025-02-18 17:30:20 +09:00
9cdb486785 [👷🏻] 채팅(Base) 수정 6차 - 로컬에서 테스트가능하게 코드 추가
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-18 01:16:17 +09:00
4b7308f9d6 [👷🏻] 채팅(Base) 수정 5차
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-18 00:57:23 +09:00
6f199389f1 [👷🏻] 채팅(Base) 롤백
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-18 00:34:22 +09:00
6f376a5d29 [👷🏻] 채팅(Base) 수정 4차
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-18 00:11:33 +09:00
c58373488d [👷🏻] 채팅(Base) 수정 3차
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-17 23:43:41 +09:00
ab045e6eb8 [👷🏻] 채팅(Base) 수정 2차 & 로컬 맥 데이터 머지 작업
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2025-02-17 22:52:44 +09:00
Seonkyu_Kim
6f06f3b39c [👷] 채팅(Base) 수정 1차 2025-02-17 17:39:42 +09:00
Seonkyu_Kim
e48eb6f76b [] SignalR을 활용한 채팅서비스(기초) 개발 및 테스트 2025-02-17 15:35:58 +09:00
a8e2a63db0 [🐛] 13차 푸시 확인 - 에러 처리 및 여러가지 코드 정리, 푸시 전송 기능 완료
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 23:21:49 +09:00
846f5c91a3 [🐛] 12차 푸시 확인 - 경로 변경
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 17:41:08 +09:00
6f9880b9d3 [] 11차 푸시 확인 - 로컬에서 수신 확인 완료
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 17:34:14 +09:00
f4efd70507 [🐛] 10차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 16:11:08 +09:00
8f7f1c4894 [🐛] 9차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 11:47:33 +09:00
aee9a805e0 [🐛] 8차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 11:29:56 +09:00
a5e1cbc347 [🐛] 7차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 11:18:59 +09:00
06507eca1f [🐛] 6차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 10:55:26 +09:00
0634f8f156 [🐛] 5차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 02:55:12 +09:00
d52ef4ff02 [🐛] 4차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 02:39:42 +09:00
39beefcd37 [🐛] 3차 푸시 확인
Signed-off-by: seonkyu.kim <sean.kk@daum.net>
2024-11-30 02:30:43 +09:00
818721f11d [🐛] 2차 APNs 확인 2024-11-30 02:13:47 +09:00
522432455c [] APNS(.p12) 개발 2024-11-30 01:09:51 +09:00
da0e142a79 [🐛] 제대로 안잡혀서 2차 수정 2024-11-29 10:54:00 +09:00
73cfac228d [🐛] API Response 의 Status에서 출력 KEY 대소문자 수정 2024-11-29 09:29:46 +09:00
ca3a9a7408 [] 각종 설정 및 Version 체크 컨트롤러 작성 2024-11-27 17:58:07 +09:00
71 changed files with 6114 additions and 58 deletions

78
.gitignore vendored
View File

@ -1,8 +1,12 @@
# 특정 환경에 따라 추가
/private/
/publish/
/bin/
/obj/
./private/
./privacy/
./publish/
publish/
./bin/
@ -15,6 +19,7 @@ publish/
*.swp
# macOS 관련 파일 제외
._
._*
.DS_Store
.AppleDouble
@ -49,6 +54,11 @@ obj/
# Blazor 관련
**/wwwroot/_framework/
./wwwroot
**/wwwroot
**/publish
./publish
# Docker 관련
docker-compose.override.yml
@ -56,4 +66,68 @@ Dockerfile
# 기타 캐시 파일
**/*.cache
**/*.tmp
**/*.tmp# 특정 환경에 따라 추가
/private/
/publish/
/bin/
/obj/
./private/
./privacy/
./publish/
./bin/
# 기본 파일 및 폴더 제외
*.log
*.env
*.bak
*.tmp
*.swp
# macOS 관련 파일 제외
._
._*
.DS_Store
.AppleDouble
.LSOverride
.Spotlight-V100
.Trashes
# Windows 관련
Thumbs.db
ehthumbs.db
desktop.ini
# Visual Studio 관련
.vscode/
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# Rider 관련
.idea/
*.sln.iml
# .NET 관련
bin/
obj/
*.pdb
*.dll
*.exe
*.nuget/
# Blazor 관련
**/wwwroot/_framework/
# Docker 관련
docker-compose.override.yml
Dockerfile
# 기타 캐시 파일
**/*.cache
**/*.tmp

View File

@ -8,8 +8,25 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Program\Controllers\V1\Interfaces\" />
<Folder Include="publish\debug\" />
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\app.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\tailwind.css" />
</ItemGroup>

77
Diary/25.03.md Normal file
View File

@ -0,0 +1,77 @@
# 2025년 3월 To-do
## 6일 (목)
### 1. PUSH API 만들기
1. [X] 푸시 목록 확인 : [./push]
2. [X] 푸시 만들기 [./push/create]
3. [ ] 푸시 삭제하기 [./push/delete]
4. [ ] 사용자가 받은 전체 푸시 확인 [./push/list]
### 2. 학원 구분이 가능하게 하는 방법으로 바꾸기
1. [x] 푸시 전송
2. [x] 푸시 변경
---
## 7일(금)
### 1. PUSH API 만들기
1. [X] 푸시 삭제하기 [./push/delete]
2. [ ] 사용자가 받은 전체 푸시 확인 [./push/list]
### 2. log 기록 남게 만들기
1. [ ] 유저 관련 테이블들 로그 기록 만들기
2. [ ] 푸시 관련 테이블들 로그 기록 만들기
### 3. 출력 및 오류 메세지 알아보기 쉽게 변경하기
1. [X] 메세지 출력에 summary 추가하기
### 4. DB 저장 & 삭제 로직 변경하기
1. [X] 저장 로직 통일하기
2. [X] 삭제 로직 통일하기
---
## 10일(월)
### 1. PUSH API 만들기
1. [X] 사용자가 받은 전체 푸시 확인 [./push/list]
2. [X] 사용자가 받은 푸시 목록 삭제 [./push/delete/list]
### 2. log 기록 남게 만들기
1. [ ] 유저 관련 테이블들 로그 기록 만들기
2. [X] 푸시 관련 테이블들 로그 기록 만들기
### 3. PUSH API 로직 변경
1. [X] 전송 로직 변경
2. [X] 케비닛 저장 로직 변경
---
## 11일(화)
### 1. USER API 점검 및 수정
1. [X] 회원 정보 조회 [./user]
2. [X] 회원 가입 [./user/register]
3. [X] 로그인 [./user/login]
4. [X] 로그아웃 [./user/logout]
5. [X] 학원 조회 [./user/academy]
### 2. USER API 로그 기록 만들기
1. [X] 필요한 위치에 등록하기
### 3. USER API 만들기
1. [X] 회원 탈퇴 [./user/cancel]
2. [ ] [보류] 회원 정보 변경 [./user/set]
- 근데 회원 정보를 변경하는게 뭐뭐를 변경해야 하는지 아직 정해진게 없어서 이건 일단 보류
---
## 19일(수)
- 모바일(iOS) 앱 작업을 진행하다가 문제를 발견해서 해결중
### 1. Header 및 접근 보안 이슈
1. [X] 헤더 접근 방식 반영하기
2. [ ] 헤더 접근 위한 고유한 값 생성하기
- 로그 저장 안되는 문제 발생
---
## 20일(수)
### 1. Header 및 접근 보안 이슈
1. [X] 헤더 접근 위한 고유값 생성 후 로그 저장에서 발생하는 이슈 확인
- dbSet에 등록을 안했던 문제였음
2. [ ] 이거 헤더 필터 제대로 안걸림
### 2. iOS 앱 과 연결
1. [ ] 앱에서 데이터 받아 사용자 정보 관리 정상적으로 작동되는지 확인

63
Diary/25.04.md Normal file
View File

@ -0,0 +1,63 @@
# 2025년 4월 To-do
## 3일 (목)
### 1. 전체적인 구조 재 정립
1. [ ] Controller, Model, Repository, Service, DTO 확립
2. [ ] 확립된 구조에 맞게 폴더 구조 변경 및 네이밍 정의
3. [ ] 변경된 구조에 맞게 코드 리팩토링
4. [ ] 응답이나 예외에 맞는 일관되게 코드 통일화
## 리팩토링
### 리팩토링의 필요성
1. 현재 C,R,S 등 의 폴더를 만들어는 뒀으나 해당 구조에 맞게 작업이 올바르게 되지 않음
2. 제대로 구분되지 않다보니 하나의 Controller 에서 다양한 역할과 책임을 맡고 있음
3. 그러다보니 명확한 확장과 구조의 파악이 어려움
### 목표
- 책임과 역할에 맞게 명확한 구분을 한다.
#### 원칙
1. Common, Controller, Model, Repository, Service, DTO 등 역할별 책임에 맞게 계층 분리
2. 도메인 중심으로 각 단위별로 묶기
### 구조 정의
```
/Controllers
└─ /V1
└─ /Interfaces
└─ I{Domain}.cs
└─ {Domain}Contreoller.cs
/Services
└─ /V1
└─ /Interfaces
└─ I{Domain}Service.cs
└─ {Domain}Service.cs
/Repositories
└─ /V1
└─ /Interfaces
└─ I{Domain}Repository.cs
└─ {Domain}Repository.cs
/Models
└─ /Entities
└─ {Domain}.cs
└─ /DTOs
└─ /V1
└─ {Domain}Dto.cs
/Common
└─ /{공통기능}
└─ {공통기능관련}.cs
SwaggerConfigure.cs
Program.cs
```
---
## 14일 (월)
### 1. 회원가입 이후 동작할 기능 구현하기
1. [ ] 학원 목록 관련 기능 추가
### 2. Blazor를 활용한 Admin 페이지 생성해보기

26
Document/Rule.md Normal file
View File

@ -0,0 +1,26 @@
# AcaMate API 문서
## 개요
## 프로젝트 구조
### 각 폴더 간 관계
#### 역할
- Controller: API 요청을 처리하고 응답을 반환하는 역할
- Service: 비즈니스 로직을 처리하는 역할
- Repository: 데이터베이스와의 상호작용을 처리하는 역할
- Model: 데이터 구조를 정의하는 역할
#### 폴더 관계
- Controller 는 Service 를 참조하고, Service 는 Repository 를 참조한다.
- Controller 는 Service 와 1:N 관계를 가진다.
- Service 는 Repository 와 1:N 관계를 가진다.
- Controller에서 Repository를 직접 참조하지 않는다.
- Repository 와 Service 는 모두 Interface 를 통해 의존성을 주입받는다.
- Common 폴더는 모든 계층에서 공통적으로 사용되는 유틸리티나 헬퍼 클래스를 포함한다.
### 오류 코드
- 0xx : 성공
- 1xx : 입력 오류
- 2xx : 출력 오류
- 3xx : 통신 오류
- 999 : 알 수 없는 오류

16
Jenkinsfile vendored
View File

@ -9,6 +9,22 @@ pipeline {
APP_VOLUME = '/src'
}
stages {
stage('Clear Repository') {
steps {
script {
sh """
echo 'Clearing Front directory'
docker run --rm -v /volume1/AcaMate/PROJECT/Application/Back:/back alpine \
sh -c "find /back -mindepth 1 -maxdepth 1 \\
! -name 'private' \\
! -name 'publish' \\
! -name 'wwwroot' \\
-exec rm -rf {} +"
echo 'Clean complete'
"""
}
}
}
stage('Clone Repository') {
steps {
git url: 'https://git.ipstein.myds.me/AcaMate/AcaMate_API.git', branch: env.GIT_BRANCH

View File

@ -1,73 +1,255 @@
//var builder = WebApplication.CreateBuilder(args);
/*
var options = new WebApplicationOptions
using Pomelo.EntityFrameworkCore;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Back;
using Back.Program.Common.Auth;
using Back.Program.Common.Auth.Interface;
using Back.Program.Common.Chat;
using Back.Program.Common.Data;
using Back.Program.Common.Middleware;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1;
using Back.Program.Services.V1.Interfaces;
Boolean isLocal = false;
// 로컬 테스트 할 때는 이거 키고 아니면 끄기
// isLocal = true;
var builder = WebApplication.CreateBuilder(args);
// DB 설정부 시작
builder.Configuration.AddJsonFile("private/dbSetting.json", optional: true, reloadOnChange: true);
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext<AppDbContext>(optionsAction: (serviceProvider, options) =>
{
WebRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"
? "/src/publish/Debug/wwwroot"
: "/src/publish/Release/wwwroot"
};
*/
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var dbName = httpContextAccessor.HttpContext?.Request.Query["aca_code"].ToString();
var baseConnectionString = builder.Configuration.GetConnectionString("MariaDbConnection");
if (!string.IsNullOrEmpty(dbName))
{
baseConnectionString = baseConnectionString.Replace("database=AcaMate", $"database={dbName}");
}
// var currentDirectory = Directory.GetCurrentDirectory();
// var webRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"
// ? Path.Combine(currentDirectory, "publish/Debug/wwwroot")
// : Path.Combine(currentDirectory, "publish/Release/wwwroot");
options.UseMySql(baseConnectionString, ServerVersion.AutoDetect(baseConnectionString));
});
var webRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_WEBROOT");
// builder.WebHost.UseWebRoot(webRootPath);
// DB 설정부 끝
var options = new WebApplicationOptions { WebRootPath = webRootPath };
var builder = WebApplication.CreateBuilder(options);
var dbString = builder.Configuration.GetConnectionString("MariaDbConnection");
var userString = builder.Configuration.GetConnectionString("DBAccount");
// var env = builder.Environment.EnvironmentName;
// string wwwrootPath = env == "Development" ? "/src/publish/Debug/wwwroot" : "/src/publish/Release/wwwroot";
// builder.WebHost.UseWebRoot(wwwrootPath);
// JWT 설정부 시작
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddJsonFile("private/jwtSetting.Development.json", optional: true, reloadOnChange: true);
}
else
{
builder.Configuration.AddJsonFile("private/jwtSetting.json", optional: true, reloadOnChange: true);
}
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings"));
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
ClockSkew = TimeSpan.FromMinutes(jwtSettings.ClockSkewMinutes)
};
});
// JWT 설정부 끝
// PUSH 설정부
// 설정 바인딩 (appsettings.json의 "ApnsPushService" 섹션)
builder.Services.Configure<PushFileSetting>(builder.Configuration.GetSection("PushFileSetting"));
// HttpClientFactory를 이용한 ApnsPushService 등록 (핸들러에 인증서 추가)
builder.Services.AddHttpClient<IPushService, PushService>(client =>
{
var settings = builder.Configuration.GetSection("PushFileSetting").Get<PushFileSetting>();
client.BaseAddress = new Uri(settings.uri);
client.Timeout = TimeSpan.FromSeconds(60);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var config = builder.Configuration.GetSection("PushFileSetting").Get<PushFileSetting>();
var handler = new HttpClientHandler();
// p12PWPath 파일에서 비밀번호 읽어오기 (예시: JSON {"Password": "비밀번호"})
var json = File.ReadAllText(config.p12PWPath);
var keys = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
var certificate = new X509Certificate2(config.p12Path, keys["Password"]);
handler.ClientCertificates.Add(certificate);
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
return handler;
});
// InMemoryPushQueue와 백그라운드 서비스 등록
builder.Services.AddSingleton<IPushQueue, InMemoryPushQueue>();
builder.Services.AddHostedService<PushBackgroundService>();
// PUSH 설정부 끝
builder.Services.AddControllers();
// 세션 설정
// IN-MEMORY 캐시
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
// ==== SCOPED 으로 등록 할 서비스 ==== //
// 여기다가 API 있는 컨트롤러들 AddScoped 하면 되는건가?
builder.Services.AddScoped<JwtTokenService>();
builder.Services.AddScoped<ILogRepository, LogRepository>();
builder.Services.AddScoped<IRepositoryService, RepositoryService>();
builder.Services.AddScoped<ISessionService, SessionService>();
builder.Services.AddScoped<IHeaderConfig, HeaderConfigRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IKakaoService, KakaoService>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IAppService, AppService>();
builder.Services.AddScoped<IAppRepository, AppRepository>();
builder.Services.AddScoped<IClassService, ClassService>();
builder.Services.AddScoped<IClassRepository, ClassRepository>();
// builder.Services.AddScoped<IPushService, PushService>();
builder.Services.AddScoped<IPushRepository, PushRepository>();
builder.Services.AddScoped<SessionManager>();
builder.Services.AddScoped<DedicateWeb>();
// builder.Services.AddScoped<UserService>(); //
// builder.Services.AddScoped<UserController>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 스웨거 설정 추가 부분
// builder.Services.AddSwaggerGen();
builder.Services.AddCustomSwagger();
// SignalR 설정 추가 부분
builder.Services.AddSignalR();
builder.Services.AddCors(option =>
{
option.AddPolicy("CorsPolicy", builder =>
{
builder
.WithOrigins("https://devacamate.ipstein.myds.me", "https://acamate.ipstein.myds.me") // 특정 도메인만 허용
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// 로그 설정 부분
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(builder.Environment.IsDevelopment() ? LogLevel.Trace : LogLevel.Warning);
if (isLocal)
{
builder.WebHost.UseUrls("http://0.0.0.0:5144");
}
else
{
builder.WebHost.UseUrls(builder.Environment.IsDevelopment()? "http://0.0.0.0:7004":"http://0.0.0.0:7003");
}
///// ===== builder 설정 부 ===== /////
var app = builder.Build();
// Configure the HTTP request pipeline.
string staticRoot;
if (isLocal)
{
staticRoot = app.Environment.IsDevelopment() ? //"/publish/debug/wwwroot" : "/publish/release/wwwroot" ;
Path.Combine(Directory.GetCurrentDirectory(), "publish", "debug", "wwwroot") : Path.Combine(Directory.GetCurrentDirectory(), "publish", "release", "wwwroot") ;
}
else
{
staticRoot = app.Environment.IsDevelopment() ?
"/src/publish/debug/wwwroot" : "/src/publish/release/wwwroot" ;
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseCustomSwaggerUI();
app.UseDeveloperExceptionPage(); // 좀더 자세한 예외 정보 제공
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
app.UseExceptionHandler("/Error");
// .UseStaticFiles()
// 로컬 테스트 위한 부분 (올릴떄는 켜두기)
// app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = true
});
app.UseRouting();
app.MapFallbackToFile("index.html");
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
FileProvider = new PhysicalFileProvider(staticRoot),
RequestPath = ""
});
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.UseRouting();
app.UseSession();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
// 헤더 미들웨어 부분
app.UseMiddleware<APIHeaderMiddleware>
((object)new string[] { "iOS_AM_Connect_Key", "And_AM_Connect_Key", "Web-AM-Connect-Key" });
app.UseWebSockets();
Console.WriteLine($"[정적 파일 경로] {staticRoot}");
app.UseEndpoints(end =>
{
ControllerEndpointRouteBuilderExtensions.MapControllers(end);
end.MapHub<ChatHub>("/chatHub");
end.MapFallback(context => { return context.Response.SendFileAsync(Path.Combine(staticRoot, "index.html")); });
});
//예외처리 미들웨어 부분
app.UseMiddleware<ExceptionMiddleware>();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@ -0,0 +1,76 @@
using Back.Program.Common.Auth.Interface;
using Back.Program.Common.Data;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Common.Auth
{
///
public class APIHeaderMiddleware
{
private readonly RequestDelegate _next;
private readonly string[] _headerNames;
// private readonly IHeaderConfig _headerConfig;
public APIHeaderMiddleware(RequestDelegate next, string[] headerNames) //, IHeaderConfig headerConfig)
{
_next = next;
_headerNames = headerNames;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Equals("/api/v1/in/app", StringComparison.OrdinalIgnoreCase))
{
await _next(context);
return;
}
if (context.Request.Path.Value != null && context.Request.Path.Value.Contains("/out/"))
{
await _next(context);
return;
}
// 정적 파일 요청은 미들웨어 건너뜀
var path = context.Request.Path.Value;
if (path != null && (path.StartsWith("/api")))
{
// Scoped 사용해서 값 가져오는 곳임
var headerConfig = context.RequestServices.GetRequiredService<IHeaderConfig>();
bool valid = false;
foreach (var header in _headerNames)
{
// context.Request.Headers.TryGetValue(header, out var headerValue)
// header 를 찾는데 header
if (context.Request.Headers.TryGetValue(header, out var headerValue) &&
!string.IsNullOrWhiteSpace(headerValue))
{
var keyName = await headerConfig.GetExpectedHeaderValueAsync(headerValue);
if (keyName != string.Empty)
{
valid = true;
break;
}
}
}
if (!valid)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync($"Invalid header value");
return;
}
await _next(context);
return;
}
await _next(context);
}
}
}

View File

@ -0,0 +1,78 @@
using System.Security.Claims;
using System.Text.Json;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Common.Auth;
public class DedicateWeb(
ILogger<DedicateWeb> _logger,
SessionManager _sessionManager,
IRepositoryService _repositoryService,
JwtTokenService _jwtTokenService,
IAppService _appService)
{
public async Task<(string code, string result)> GetAuthToken()
{
var summary = "GetAuthToken";
try
{
// 1. 세션에서 토큰 가져오기
var (result, token) = await _sessionManager.GetString("token");
_logger.LogInformation($"세션에서 토큰 가져오기 결과: {result}, 토큰: {token}");
if (!result || string.IsNullOrEmpty(token))
{
_logger.LogWarning($"세션에 토큰이 없습니다");
return ("200", "세션에 토큰 없음");
}
// 2. 토큰 검증
var validToken = await _jwtTokenService.ValidateToken(token);
_logger.LogInformation($"토큰 검증 결과: {validToken != null}");
if (validToken == null)
{
// 3. 토큰이 유효하지 않으면 리프레시 토큰으로 새 토큰 발급 시도
var (refreshResult, refreshToken) = await _sessionManager.GetString("refresh");
_logger.LogInformation($"리프레시 토큰 가져오기 결과: {refreshResult}, 토큰: {refreshToken}");
if (!refreshResult || string.IsNullOrEmpty(refreshToken))
{
_logger.LogWarning($"리프레시 토큰이 없습니다");
return ("200", "리프레시 토큰 없음");
}
// 4. 리프레시 토큰으로 새 토큰 발급
var retryResult = await _appService.RetryAccess(summary, refreshToken);
_logger.LogInformation($"토큰 재발급 결과: {retryResult.status.code}");
if (retryResult.status.code == "000")
{
// 5. 새 토큰을 세션에 저장
var data = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(retryResult.data));
var newToken = data.GetProperty("access").GetString();
await _sessionManager.SetString("token", newToken);
_logger.LogInformation($"[{summary}] 새 토큰 세션 저장 완료");
return ("000", newToken);
}
else
{
_logger.LogWarning($"[{summary}] 토큰 갱신 실패: {retryResult.status.message}");
return ("102", "토큰 갱신 실패");
}
}
return ("000", token);
}
catch (Exception ex)
{
_logger.LogError($"[{summary}] 세션 데이터 조회 중 오류: {ex.Message}");
_logger.LogError($"[{summary}] 스택 트레이스: {ex.StackTrace}");
return ("100", "세션 데이터 조회 중 오류");
}
}
}

View File

@ -0,0 +1,27 @@
using Back.Program.Common.Auth.Interface;
using Back.Program.Common.Data;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Common.Auth
{
/// <summary>
/// DB에서 헤더 키값 찾아서 그 밸류 값 빼오기 위해서 사용
/// </summary>
public class HeaderConfigRepository : IHeaderConfig
{
private readonly AppDbContext _dbContext;
public HeaderConfigRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<string> GetExpectedHeaderValueAsync(string headerValue)
{
var config = await _dbContext.APIHeader
.FirstOrDefaultAsync(h => h.h_value == headerValue);
return config?.h_key ?? string.Empty;
}
}
}

View File

@ -0,0 +1,7 @@
namespace Back.Program.Common.Auth.Interface
{
public interface IHeaderConfig
{
Task<string> GetExpectedHeaderValueAsync(string headerName);
}
}

View File

@ -0,0 +1,110 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Back.Program.Common.Model;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Back.Program.Common.Auth
{
/// <summary>
/// 사용자의 정보를 바탕으로 JWT 생성
/// </summary>
public class JwtTokenService
{
private readonly JwtSettings _jwtSettings;
private readonly ILogger<JwtTokenService> _logger;
public JwtTokenService(IOptions<JwtSettings> jwtSettings, ILogger<JwtTokenService> logger)
{
_jwtSettings = jwtSettings.Value;
_logger = logger;
}
// JWT 토큰 생성
public string GenerateJwtToken(string jwtKey)//, string role)
{
// 1. 클레임(Claim) 설정 - 필요에 따라 추가 정보도 포함
var claims = new List<Claim>
{
// 토큰 주체(sub) 생성을 위해 값으로 uid를 사용함 : 토큰이 대표하는 고유 식별자
new Claim(JwtRegisteredClaimNames.Sub, jwtKey),
// Jti 는 토큰 식별자로 토큰의 고유 ID 이다.
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// jwt 토큰이 가지는 권한
// new Claim(ClaimTypes.Role, role),
// 추가 클레임 예: new Claim(ClaimTypes.Role, "Admin")
};
// 2. 비밀 키와 SigningCredentials 생성
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 3. 토큰 생성 (Issuer, Audience, 만료 시간, 클레임, 서명 정보 포함)
var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(_jwtSettings.ExpiryMinutes),
signingCredentials: credentials
);
// 4. 토큰 객체를 문자열로 변환하여 반환
return new JwtSecurityTokenHandler().WriteToken(token);
}
// 리프레시 토큰 생성
public RefreshToken GenerateRefreshToken(string uid)
{
var randomNumber = new byte[32]; // 256비트
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
}
return new RefreshToken()
{
uid = uid,
refresh_token = Convert.ToBase64String(randomNumber),
create_Date = DateTime.UtcNow,
expire_date = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiryDays)
};
}
/// <summary>
/// 여기는 엑세스 토큰의 확인을 위한 jwt 서비스 내의 인증 메서드
/// </summary>
public async Task<ClaimsPrincipal?> ValidateToken(string token)
{
if (string.IsNullOrWhiteSpace(token)) return null;
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var key = Encoding.UTF8.GetBytes(_jwtSettings.SecretKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = _jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSettings.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(_jwtSettings.ClockSkewMinutes)
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out var securityToken);
return principal;
}
catch (Exception ex)
{
_logger.LogError($"엑세스 토큰 오류: {ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,57 @@
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.SignalR;
namespace Back.Program.Common.Chat;
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
private readonly IChatService _chatService;
public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
{
_logger = logger;
_chatService = chatService;
}
// 클라이언트에서 메시지를 보내면 모든 사용자에게 전송
public async Task SendMessage(string user, string message)
{
Console.WriteLine($"Message received: {user}: {message}");
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// 특정 사용자에게 메시지를 보냄
public async Task SendMessageToUser(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", message);
}
// 클라이언트가 연결될 때 호출
public override async Task OnConnectedAsync()
{
await Clients.Caller.SendAsync("ReceiveMessage", "System", $"Welcome! Your ID: {Context.ConnectionId}");
Console.WriteLine("OnConnectedAsync");
await base.OnConnectedAsync();
}
// 클라이언트가 연결 해제될 때 호출
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.All.SendAsync("ReceiveMessage", "System", $"{Context.ConnectionId} disconnected");
Console.WriteLine("OnDisconnectedAsync");
await base.OnDisconnectedAsync(exception);
}
public async Task JoinRoom(string cid)
{
await Groups.AddToGroupAsync(Context.ConnectionId, cid);
}
public async Task JoinGroup(string cid, string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
}

View File

@ -0,0 +1,70 @@
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Microsoft.EntityFrameworkCore;
using Version = Back.Program.Models.Entities.Version;
namespace Back.Program.Common.Data
{
//database=AcaMate;
public class AppDbContext: DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
//MARK: API
public DbSet<APIHeader> APIHeader { get; set; }
//MARK: Program
public DbSet<Version> Version { get; set; }
public DbSet<Academy> Academy { get; set; }
public DbSet<RefreshToken> RefreshToken { get; set; }
//MARK: USER
public DbSet<Login> Login { get; set; }
public DbSet<User_Academy> UserAcademy { get; set; }
public DbSet<User> User { get; set; }
public DbSet<Permission> Permission { get; set; }
// public DbSet<Token> Token { get; set; }
public DbSet<Location> Location { get; set; }
public DbSet<Contact> Contact { get; set; }
//MARK: PUSH
public DbSet<DBPayload> DBPayload { get; set; }
public DbSet<PushCabinet> PushCabinet { get; set; }
//MARK: CLASS
public DbSet<Class_Info> Class_Info { get; set; }
public DbSet<Class_Attendance> Class_Attendance { get; set; }
public DbSet<Class_Map> Class_Map { get; set; }
//MARK: CHATTING
// public DbSet<>
//MARK: LOG
public DbSet<LogPush> LogPush { get; set; }
public DbSet<LogUser> LogUser { get; set; }
public DbSet<LogProject> LogProject { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User_Academy>()
.HasKey(ua => new { ua.uid, ua.bid });
// modelBuilder.Entity<PushCabinet>()
// .HasKey(c => new { c.uid, c.bid, c.pid });
modelBuilder.Entity<DBPayload>()
.HasKey(p => new { p.bid, p.pid });
// modelBuilder.Entity<LogPush>().HasNoKey();
modelBuilder.Entity<Class_Attendance>()
.HasKey(ca => new { ca.cid, ca.uid, ca.attendace_date});
modelBuilder.Entity<Class_Map>()
.HasKey(ca => new { ca.cid, ca.uid});
}
}
}

View File

@ -0,0 +1,53 @@
namespace Back.Program.Common.Data;
public class SessionManager
{
private readonly IHttpContextAccessor _http;
public SessionManager(IHttpContextAccessor http)
{
_http = http;
}
public Task<bool> SetString(string key, string value)
{
try
{
_http.HttpContext.Session.SetString(key, value);
return Task.FromResult(true);
}
catch
{
return Task.FromResult(false);
}
}
public Task<(bool result, string data)> GetString(string key)
{
try
{
var value = _http.HttpContext.Session.GetString(key);
return Task.FromResult((true, value ?? string.Empty));
}
catch
{
return Task.FromResult((false, ""));
}
}
public Task<bool> Remove(string key)
{
try
{
_http.HttpContext.Session.Remove(key);
return Task.FromResult(true);
}
catch
{
return Task.FromResult(false);
}
}
}

View File

@ -0,0 +1,49 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using Back.Program.Common.Model;
namespace Back.Program.Common.Middleware;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context); // 다음 미들웨어 호출
}
catch (AcaException ex)
{
_logger.LogWarning(ex, $"예외 발생 : {ex.Message}");
// 400 : 이건 개발자가 직접 던지는 비즈니스 로직에 대한 예외 == 클라이언트의 오류
context.Response.StatusCode = ex.HttpStatus;
context.Response.ContentType = "application/json; charset=utf-8";
var response = APIResponse.Send<string>(ex.Code, ex.Message, string.Empty);
var json = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(json);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled Exception");
context.Response.StatusCode = 500;
var response = APIResponse.InternalSeverError("서버 내부 오류가 발생했습니다.");
var json = JsonSerializer.Serialize(response);
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(json);
}
}
}

View File

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Common.Model
{
[Table("api_header")]
public class APIHeader
{
[Key]
public string specific_id { get; set; }
public DateTime connect_date { get; set; }
public string h_key { get; set; }
public string h_value { get; set; }
}
public class SessionData
{
public string key { get; set; }
public string value { get; set; }
}
}
/*
h_key : h_value
iOS_AM_Connect_Key
And_AM_Connect_Key
Web-AM-Connect-Key
*/

View File

@ -0,0 +1,25 @@
using System.ComponentModel;
namespace Back.Program.Common.Model;
public static class ResposeCode
{
public const string Success = "000";
public const string InputErr = "100";
public const string OutputErr = "200";
public const string NetworkErr = "300";
public const string UnknownErr = "999";
}
public class AcaException : Exception
{
public string Code { get; }
public int HttpStatus { get; }
public AcaException(string code, string message, int httpStatus = 400) : base(message)
{
this.Code = code;
this.HttpStatus = httpStatus;
}
}

View File

@ -0,0 +1,54 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Common.Model
{
public class JwtSettings
{
public string SecretKey { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public int ExpiryMinutes { get; set; }
public int ClockSkewMinutes { get; set; }
public int RefreshTokenExpiryDays { get; set; }
}
[Table("refresh_token")]
public class RefreshToken
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
public string refresh_token { get; set; }
public DateTime create_Date { get; set; }
public DateTime expire_date { get; set; }
// 이건 로그아웃시에 폐기 시킬예정이니 그떄 변경하는걸로 합시다.
public DateTime? revoke_Date { get; set; }
}
public class ValidateToken
{
public string token { get; set; }
public string refresh { get; set; }
public string uid { get; set; }
}
}
/*
"""
1. .
2. DB에 .
3. .
4. .
5. (1) .
6. (2) .
7. .
8. .
9. .
10. 2 .
"""
*/

View File

@ -0,0 +1,74 @@
using System.Text.Json;
namespace Back.Program.Common.Model
{
public class APIResponseStatus<T>
{
public Status status { get; set; }
public T? data { get; set; }
public string JsonToString()
{
return JsonSerializer.Serialize(this);
}
}
public class Status
{
public string code { get; set; }
public string message { get; set; }
}
public static class APIResponse
{
public static APIResponseStatus<T> Send<T>(string code, string message, T data)
{
return new APIResponseStatus<T>
{
status = new Status()
{
code = code,
message = message
},
data = data
};
}
/// <summary>
/// 반환값 없는 API 정상 동작시
/// </summary>
public static APIResponseStatus<object> Success (){
return Send<object>("000", "정상", new {});
}
public static APIResponseStatus<object> InvalidInputError(string? msg = null)
{
return Send<object>("100", msg ?? "입력 값이 유효하지 않습니다.", new {});
}
public static APIResponseStatus<object> AccessExpireError(string? msg = null)
{
return Send<object>("101", msg ?? "엑세스 토큰이 유효하지 않습니다.", new {});
}
// -- -- -- OUTPUT ERROR -- -- -- //
public static APIResponseStatus<object> NotFoundError(string? msg = null)
{
return Send<object>("200", msg ?? "알맞은 값을 찾을 수 없습니다.", new {});
}
public static APIResponseStatus<object> InternalSeverError(string? msg = null)
{
return Send<object>("300", msg ?? "통신에 오류가 발생하였습니다.", new {});
}
public static APIResponseStatus<object> UnknownError(string? msg = null)
{
return Send<object>("999", msg ?? "알 수 없는 오류가 발생하였습니다.", new {});
}
}
}

View File

@ -0,0 +1,123 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1;
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Version = Back.Program.Models.Entities.Version;
namespace Back.Program.Controllers.V1
{
[ApiController]
[Route("/api/v1/in/app")]
[ApiExplorerSettings(GroupName = "공통")]
public class AppController : ControllerBase
{
private readonly AppDbContext _dbContext;
private readonly ILogger<AppController> _logger;
private readonly IRepositoryService _repositoryService;
private readonly JwtTokenService _jwtTokenService;
private readonly IAppService _appService;
private readonly IAppRepository _appRepository;
private readonly SessionManager _sessionManager;
public AppController(AppDbContext dbContext, ILogger<AppController> logger, IRepositoryService repositoryService,
JwtTokenService jwtTokenService, IAppService appService, IAppRepository appRepository, SessionManager sessionManager)
{
_dbContext = dbContext;
_logger = logger;
_repositoryService = repositoryService;
_jwtTokenService = jwtTokenService;
_appService = appService;
_appRepository = appRepository;
_sessionManager = sessionManager;
}
// 이 키값의 제한 시간은 24h이다
[HttpGet]
[CustomOperation("헤더 정보 생성", "헤더에 접근하기 위한 키 값 받아오기", "시스템")]
public async Task<IActionResult> GetHeaderValue(string type, string specific, string project)
{
if (string.IsNullOrEmpty(specific) || string.IsNullOrEmpty(type) || string.IsNullOrEmpty(project))
return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(AppController), "GetHeaderValue");
var result = await _appService.GetHeader(summary, type, specific, project);
return Ok(result);
}
[HttpGet("version")]
[CustomOperation("앱 버전 확인", "앱 버전을 확인해서 업데이트 여부 판단", "시스템")]
public async Task<IActionResult> GetVersionData(string type)
{
if (string.IsNullOrEmpty(type)) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(AppController), "GetHeaderValue");
var result = await _appService.GetVersion(summary, type);
return Ok(result);
}
[HttpGet("retryAccess")]
[CustomOperation("엑세스 토큰 재발급", "액세스 토큰 재발급 동작 수행", "시스템")]
public async Task<IActionResult> RetryAccessToken(string refresh)
{
if (string.IsNullOrEmpty(refresh)) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(AppController), "RetryAccessToken");
var result = await _appService.RetryAccess(summary, refresh);
return Ok(result);
}
[HttpGet("session/get")]
[CustomOperation("세션 정보 읽어오기", "세션 정보를 읽어오는 동작 수행", "시스템")]
public async Task<IActionResult> GetSessionData(string key)
{
if (string.IsNullOrEmpty(key))
{
return BadRequest(APIResponse.InvalidInputError());
}
var (success, value) = await _sessionManager.GetString(key);
if (!success)
{
return BadRequest(APIResponse.InvalidInputError());
}
string summary = _repositoryService.ReadSummary(typeof(AppController), "GetSessionData");
return Ok(APIResponse.Send("000", $"[{summary}], 정상", new { data = value }));
}
[HttpPost("session/set")]
[CustomOperation("세션 정보 저장하기", "세션 정보에 저장하는 동작 수행", "시스템")]
public async Task<IActionResult> SetSessionData([FromBody] SessionData[] requests)
{
if(requests == null || requests.Length == 0)
{
return BadRequest(APIResponse.InvalidInputError());
}
Console.WriteLine($"받은 세션 데이터: {JsonSerializer.Serialize(requests)}");
foreach(var request in requests)
{
Console.WriteLine($"세션 저장 시도 - key: {request.key}, value: {request.value}");
var success = await _sessionManager.SetString(request.key, request.value);
if (!success)
{
Console.WriteLine($"세션 저장 실패 - key: {request.key}");
return BadRequest(APIResponse.InvalidInputError());
}
Console.WriteLine($"세션 저장 성공 - key: {request.key}");
}
return Ok(APIResponse.Send("000", $"[세션 저장]: 정상", new { }));
}
}
}

View File

@ -0,0 +1,6 @@
namespace Back.Program.Controllers.V1;
public class ChatController
{
}

View File

@ -0,0 +1,31 @@
using Back.Program.Common.Model;
using Microsoft.AspNetCore.Mvc;
using Back.Program.Services.V1.Interfaces;
using Back.Program.Repositories.V1.Interfaces;
using Microsoft.AspNetCore.Http.HttpResults;
namespace Back.Program.Controllers.V1;
[ApiController]
[Route("/api/v1/in/class")]
[ApiExplorerSettings(GroupName = "수업 관리")]
public class ClassController(
ILogger<ClassController> logger,
// SessionManager sessionManager,
// DedicateWeb dedicateWeb,
IRepositoryService repositoryService,
IClassService classService)
: ControllerBase
{
// [HttpGet("info")]
[HttpGet]
[CustomOperation("수업 정보 조회", "수업 정보 조회", "수업관리")]
public async Task<IActionResult> GetClassInfo(string cid)
{
if (string.IsNullOrEmpty(cid)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(ClassController), "GetClassInfo");
var result = await classService.GetClassInfo(summary, cid);
return Ok(result);
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace Back.Program.Controllers.V1
{
[ApiController]
[Route("/api/error")]
public class ErrorController: ControllerBase
{
[HttpGet]
public IActionResult HandleError()
{
return Problem("오류가 발생하였습니다. 잠시후 다시 시도해주세요.");
}
}
}

View File

@ -0,0 +1,48 @@
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Services.V1;
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace Back.Program.Controllers.V1
{
[ApiController]
[Route("/api/v1/in/member")]
[ApiExplorerSettings(GroupName = "사업자 정보")]
public class MemberController: ControllerBase
{
private readonly ILogger<MemberController> _logger;
private readonly AppDbContext _dbContext;
private readonly IRepositoryService _repositoryService;
private readonly JwtTokenService _jwtTokenService;
public MemberController(AppDbContext dbContext, ILogger<MemberController> logger, IRepositoryService repositoryService, JwtTokenService jwtTokenService)
{
_dbContext = dbContext;
_logger = logger;
_repositoryService = repositoryService;
_jwtTokenService = jwtTokenService;
}
[HttpGet("business")]
public IActionResult GetBusinessData()
{
// return Ok("GOOD");
return Ok("DB 참조");
}
// -- -- -- -- -- -- -- -- -- -- -- -- //
[HttpGet("/api/v1/out/member/business")]
public IActionResult SearchBusinessNo()
{
return Ok("외부 참조");
}
}
}

View File

@ -0,0 +1,148 @@
using System.Text.Json;
using Back.Program.Common.Data;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Back.Program.Common.Model;
using Back.Program.Controllers.V1;
using Back.Program.Services.V1;
using Back.Program.Services.V1.Interfaces;
using Back.Program.Models.APIResponses;
namespace Back.Program.Controllers;
// TO-DO: 여기 controller, service, repository 분리 필요
[ApiController]
[Route("/api/v1/out/user")]
[ApiExplorerSettings(GroupName = "외부 동작(사용자)")]
public class OutController: ControllerBase
{
private readonly ILogger<OutController> _logger;
private readonly IRepositoryService _repositoryService;
private readonly IUserService _userService;
private readonly IKakaoService _kakaoService;
private readonly SessionManager _sessionManager;
public OutController(ILogger<OutController> logger,
IRepositoryService repositoryService, IUserService userService, IKakaoService kakaoService, SessionManager sessionManager)
{
_logger = logger;
_repositoryService = repositoryService;
_userService = userService;
_kakaoService = kakaoService;
_sessionManager = sessionManager;
}
[HttpGet("kakao/auth")]
[CustomOperation("카카오 로그인", "카카오 로그인 동작", "사용자")]
public async Task<IActionResult> KakaoLogin([FromQuery] string? scope, [FromQuery] string? redirectPath)
{
if (!string.IsNullOrEmpty(redirectPath))
{
await _sessionManager.SetString("redirectPath", redirectPath);
}
var url = await _kakaoService.GetAuthorizationUrl(scope ?? "");
Console.WriteLine($"카카오 로그인 API: {url}");
return Ok(new { url });
}
[HttpGet("kakao/redirect")]
public async Task<IActionResult> RedirectFromKakao([FromQuery] string code)
{
_logger.LogInformation("카카오 리다이렉트 시작");
var (success, response) = await _kakaoService.Redirect(code);
_logger.LogInformation($"리다이렉트 결과: {success}, 응답: {response}");
if (success)
{
var (idSuccess, idResponse) = await _kakaoService.UserMe(response);
_logger.LogInformation($"사용자 정보 조회 결과: {idSuccess}, 응답: {idResponse}");
if (idSuccess)
{
var json = JsonDocument.Parse(idResponse);
if (json.RootElement.TryGetProperty("id", out var idElement))
{
var snsId = idElement.ToString();
_logger.LogInformation($"카카오 ID: {snsId}");
var loginResult = await _userService.Login("SNS Login", "ST01", snsId);
_logger.LogInformation($"로그인 결과: {loginResult.JsonToString()}");
if (loginResult.status.code == "000")
{
var data = JsonSerializer.Deserialize<LoginAPIResponse>(JsonSerializer.Serialize(loginResult.data));
_logger.LogInformation($"로그인 데이터: {JsonSerializer.Serialize(data)}");
if (data != null)
{
string token = data.token;
string refresh = data.refresh;
_logger.LogInformation($"토큰 저장 시도 - token: {token}, refresh: {refresh}");
if (await _sessionManager.SetString("token", token) &&
await _sessionManager.SetString("refresh", refresh))
{
_logger.LogInformation("세션 저장 성공");
var (hasPath, redirectPath) = await _sessionManager.GetString("redirectPath");
await _sessionManager.Remove("redirectPath"); // 사용 후 세션에서 제거
var redirectUrl = hasPath && !string.IsNullOrEmpty(redirectPath)
? $"{redirectPath}?auth=true"
: "/about?auth=true";
_logger.LogInformation($"리다이렉트 URL: {redirectUrl}");
return Redirect(redirectUrl);
}
else
{
_logger.LogError("세션 저장 실패");
}
}
else
{
_logger.LogError("로그인 데이터가 null입니다");
}
}
else if (loginResult.status.code == "001")
{
_logger.LogInformation("회원가입 필요");
if (await _sessionManager.SetString("snsId", snsId))
{
return Redirect("/auth/register");
}
}
else
{
_logger.LogError($"로그인 실패: {loginResult.status.message}");
return BadRequest(new { error = "로그인 실패", message = loginResult.status.message });
}
}
else
{
_logger.LogError("카카오 ID를 찾을 수 없습니다");
}
}
else
{
_logger.LogError($"사용자 정보 조회 실패: {idResponse}");
}
}
else
{
_logger.LogError($"카카오 리다이렉트 실패: {response}");
}
return BadRequest();
}
// // 로그아웃 API 예시 (이미 있다면 해당 위치에 추가)
// [HttpGet("logout")]
// public IActionResult Logout()
// {
// // 세션/쿠키 등 로그아웃 처리
// Response.Cookies.Delete("IsLogin");
// // 기타 로그아웃 처리 로직...
// return Redirect("/");
// }
}

View File

@ -0,0 +1,133 @@
using System.Security.Claims;
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Services.V1;
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Controllers.V1
{
[ApiController]
[Route("/api/v1/in/push")]
[ApiExplorerSettings(GroupName = "공통")]
public class PushController : ControllerBase
{
private readonly ILogger<PushController> _logger;
private readonly IRepositoryService _repositoryService;
private readonly IPushQueue _pushQueue;
private readonly IPushService _pushService;
public PushController(ILogger<PushController> logger, IRepositoryService repositoryService,
IPushQueue pushQueue, IPushService pushService)
{
_logger = logger;
_repositoryService = repositoryService;
_pushQueue = pushQueue;
_pushService = pushService;
}
// 추가 사항
// 카테고리 별 조회 하는 부분도 추가를 할 지 고민을 해야 할 것 같음
[HttpGet()]
[CustomOperation("푸시 확인", "저장된 양식을 확인 할 수 있다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> GetPush(string bid, string? pid, string? category)
{
if (string.IsNullOrEmpty(bid)) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "GetPush");
var result = await _pushService.GetPush(summary, bid, pid, category);
return Ok(result);
}
[HttpPost("send")]
[CustomOperation("푸시 발송", "저장된 양식으로, 사용자에게 푸시를 송신한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SendPush([FromBody] PushRequest pushRequest)
{
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "SendPush");
var result = await _pushService.SendPush(summary, pushRequest);
return Ok(result);
}
[HttpPost("set")]
[CustomOperation("푸시 변경", "저장된 양식을 변경한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SetPush(string token, [FromBody] DBPayload request)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "SetPush");
var result = await _pushService.SetPush(summary, token, request);
return Ok(result);
}
[HttpPost("create")]
[CustomOperation("푸시 생성", "새로운 푸시 양식을 생성한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> CreatePush(string token, [FromBody] CreatePush request)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "CreatePush");
var result = await _pushService.CreatePush(summary, token, request);
return Ok(result);
}
[HttpDelete("delete")]
[CustomOperation("푸시 삭제", "저장된 푸시 양식을 삭제 한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> DeletePush(string token, string bid, string pid)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "DeletePush");
var result = await _pushService.DeletePush(summary,token,bid,pid);
return Ok(result);
}
[HttpDelete("delete/list")]
[CustomOperation("사용자 푸시 목록 삭제", "사용자가 받은 푸시목록에서 푸시를 삭제한다..", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> DeleteListPush(string token, int id)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "DeleteListPush");
var result = await _pushService.DeleteListPush(summary, token, id);
return Ok(result);
}
[HttpPost("list")]
[CustomOperation("사용자 푸시 목록 조회", "해당 사용자가 받은 푸시의 정보를 조회한다.", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SearchToUserPush(string token, int size, [FromBody] PushCabinet? request)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = _repositoryService.ReadSummary(typeof(PushController), "SearchToUserPush");
var result = await _pushService.SearchToUserPush(summary, token, size, request);
return Ok(result);
}
}
} // END PUSH CONTROLLER

View File

@ -0,0 +1,37 @@
using Back.Program.Common.Data;
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace Back.Program.Controllers.V1;
/// <summary>
/// USER는 사용자가 자신의 데이터를 보거나 만들거나 하는 등 직접 사용하는 경우에 사용
/// </summary>
[ApiController]
[Route("/api/v1/in/user")]
[ApiExplorerSettings(GroupName = "")]
public class SessionController : ControllerBase
{
private readonly ILogger<SessionController> _logger;
private readonly IRepositoryService _repositoryService;
private readonly SessionManager _sessionManager;
private readonly ISessionService _sessionService;
private SessionController(ILogger<SessionController> logger,
IRepositoryService repositoryService, SessionManager sessionManager, ISessionService sessionService)
{
_logger = logger;
_repositoryService = repositoryService;
_sessionManager = sessionManager;
_sessionService = sessionService;
}
[HttpGet("session/user")]
[CustomOperation("세션 정보 확인", "세션 정보 확인", "사용자")]
public async Task<IActionResult> GetSessionData()
{
string summary = _repositoryService.ReadSummary(typeof(UserController), "GetSessionData");
var result = await _sessionService.GetSessionData(summary);
return Ok(result);
}
}

View File

@ -0,0 +1,193 @@
using System.Security.Claims;
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Services.V1.Interfaces;
using Back.Program.Services.V1;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Controllers.V1
{
/// <summary>
/// USER는 사용자가 자신의 데이터를 보거나 만들거나 하는 등 직접 사용하는 경우에 사용
/// </summary>
[ApiController]
[Route("/api/v1/in/user")]
[ApiExplorerSettings(GroupName = "사용자")]
public class UserController(
ILogger<UserController> logger,
SessionManager sessionManager,
DedicateWeb dedicateWeb,
IRepositoryService repositoryService,
IUserService userService)
: ControllerBase
{
private readonly ILogger<UserController> _logger = logger;
private readonly SessionManager _sessionManager = sessionManager;
[HttpGet]
[CustomOperation("회원 정보 조회", "회원 정보 조회 (자기자신)", "사용자")]
public async Task<IActionResult> GetUserData(string token)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "GetUserData");
if (token == "VO00")
{
var (code, WebAuthResult) = await dedicateWeb.GetAuthToken();
if (code != "000") return Ok(APIResponse.Send(code, $"{WebAuthResult}", new { }));
token = WebAuthResult;
}
var result = await userService.GetUser(summary, token);
return Ok(result);
}
[HttpGet("login")]
[CustomOperation("SNS 로그인", "로그인 후 회원이 있는지 확인", "사용자")]
public async Task<IActionResult> Login(string accType, string snsId)
{
// API 동작 파라미터 입력 값 확인
if (string.IsNullOrEmpty(accType) && string.IsNullOrEmpty(snsId)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "Login");
var result = await userService.Login(summary, accType, snsId);
return Ok(result);
}
[HttpPost("register")]
[CustomOperation("회원 가입", "사용자 회원 가입", "사용자")]
public async Task<IActionResult> UserRegister([FromBody] UserAll request)
{
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "UserRegister");
var result = await userService.Register(summary, request);
return Ok(result);
}
[HttpGet("logout")]
[CustomOperation("로그아웃", "사용자 로그아웃", "사용자")]
public async Task<IActionResult> Logout(string token)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "Logout");
if (token == "VO00")
{
var (code, WebAuthResult) = await dedicateWeb.GetAuthToken();
if (code != "000") return Ok(APIResponse.Send(code, $"{WebAuthResult}", new { }));
token = WebAuthResult;
}
var result = await userService.Logout(summary, token);
return Ok(result);
}
[HttpGet("cancel")]
[CustomOperation("회원 탈퇴", "사용자 탈퇴", "사용자")]
public async Task<IActionResult> Cancel(string token)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "Cancel");
if (token == "VO00")
{
var (code, WebAuthResult) = await dedicateWeb.GetAuthToken();
if (code != "000") return Ok(APIResponse.Send(code, $"{WebAuthResult}", new { }));
token = WebAuthResult;
}
var result = await userService.Cancel(summary, token);
return Ok(result);
}
[HttpGet("academy")]
[CustomOperation("학원 리스트 확인", "사용자가 등록된 학원 리스트 확인", "사용자")]
public async Task<IActionResult> GetAcademyData(string token)
{
if (string.IsNullOrEmpty(token)) return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = repositoryService.ReadSummary(typeof(UserController), "GetAcademyData");
if (token == "VO00")
{
var (code, WebAuthResult) = await dedicateWeb.GetAuthToken();
if (code != "000") return Ok(APIResponse.Send(code, $"{WebAuthResult}", new { }));
token = WebAuthResult;
}
var result = await userService.GetAcademy(summary, token);
return Ok(result);
}
}
}
// 근데 회원 정보를 변경하는게 뭐뭐를 변경해야 하는지 아직 정해진게 없어서 이건 일단 보류
/*
[HttpGet("set")]
[CustomOperation("회원 정보 변경", "회원 정보 변경", "사혹자")]
public async Task<IActionResult> SetUserData(string token, string refresh) //, [FromBody])
{
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh))
return BadRequest(APIResponse.InvalidInputError());
if (!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
string summary = String.Empty;
try
{
summary = _repositoryService.ReadSummary(typeof(UserController), "Cancel");
// 여기서 애초에 토큰 관련 에러가 2개가 나오게 만들어져 있음
var validateToken = await _repositoryService.ValidateToken(token, refresh);
var user = await _dbContext.User.FirstOrDefaultAsync(u => u.uid == validateToken.uid);
}
catch (TokenException tokenEx)
{
return Ok(APIResponse.Send("101", $"[{summary}], 입력 받은 토큰의 문제", Empty));
}
catch (RefreshRevokeException refreshEx)
{
return Ok(APIResponse.Send("102", $"[{summary}], 폐기된 리프레시 토큰", Empty));
}
catch (Exception ex)
{
return StatusCode(500, APIResponse.UnknownError($"[{summary}], {ex.Message}"));
}
}
}
/*
string uid = "";
if (token == "System") uid = "System";
else {
if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(refresh)) return BadRequest(APIResponse.InvalidInputError());
if(!ModelState.IsValid) return BadRequest(APIResponse.InvalidInputError());
var validateToken = await _repositoryService.ValidateToken(token, refresh);
uid = validateToken.uid;
}
string summary = String.Empty;
try
{
summary = _repositoryService.ReadSummary(typeof(PushController), "GetUserData");
}
*/

View File

@ -0,0 +1,7 @@
namespace Back.Program.Models.APIResponses;
public class LoginAPIResponse
{
public string token { get; set; } = string.Empty;
public string refresh { get; set; } = string.Empty;
}

View File

@ -0,0 +1,16 @@
using System.Text.Json;
namespace Back.Program.Models.Entities
{
public class APIResult
{
public bool Success { get; set; }
public string Code { get; set; }
public string Message { get; set; }
public string JsonToString()
{
return JsonSerializer.Serialize(this);
}
}
}

View File

@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("academy")]
public class Academy
{
[Key]
public string bid { get; set; }
public string business_name { get; set; }
public string business_owner { get; set; }
public string business_number { get; set; }
public DateTime business_date { get; set; }
public string business_address { get; set; }
public string business_contact { get; set; }
public string uid { get; set; }
}
// -- -- -- -- -- DB 테이블 -- -- -- -- -- //
public class AcademyName
{
public string bid { get; set; }
public string name { get; set; }
}
public class RequestAcademy
{
public List<string> bids { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("authkey")]
public class AuthKey
{
}
}

View File

@ -0,0 +1,147 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("chat_room")]
public class Chat_Room
{
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(16)]
public required string cid { get; set; } // bid + yyyyMMdd + count
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(6)]
public required string bid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required string name { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(4)]
public required string type { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required DateTime create_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required DateTime open_date { get; set; }
public DateTime? close_date { get; set; }
public Chat_Room(string cid, string bid, string name, string type, DateTime create_date, DateTime open_date)
{
this.cid = cid;
this.bid = bid;
this.name = name;
this.type = type;
this.create_date = create_date;
this.open_date = open_date;
}
}
[Table("chat_join")]
public class Chat_Join
{
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(16)]
public required string cid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; }
public DateTime? join_date { get; set; }
public string? mid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required bool is_notice { get; set; }
public Chat_Join(string cid, string uid, bool is_notice)
{
this.cid = cid;
this.uid = uid;
this.is_notice = is_notice;
}
}
[Table("chat_message")]
public class Chat_Message
{
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(36)]
public required string mid { get; set; } // UUID
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(16)]
public required string cid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required DateTime create_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required string content { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required bool is_hidden { get; set; }
public string? media_url { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required int read_count { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required bool is_blind { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required bool check_report { get; set; }
public Chat_Message(string mid, string cid, string uid, DateTime create_date, string content, bool is_hidden,
int read_count, bool is_blind, bool check_report)
{
this.mid = mid;
this.cid = cid;
this.uid = uid;
this.create_date = create_date;
this.content = content;
this.is_hidden = is_hidden;
this.read_count = read_count;
this.check_report = check_report;
this.is_blind = is_blind;
this.check_report = check_report;
}
}
[Table("chat_report_list")]
public class Chat_Report_List
{
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(36)]
public required string mid { get; set; } // UUID
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(16)]
public required string cid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required string report { get; set; }
public Chat_Report_List(string mid, string cid, string uid, string report)
{
this.mid = mid;
this.cid = cid;
this.uid = uid;
this.report = report;
}
}
}

View File

@ -0,0 +1,68 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities;
[Table("class_info")]
public class Class_Info
{
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(10)]
public required string id { get; set; } // AMC + 4숫자 + 3대문자
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(100)]
public required string name { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; } // 담당 선생님 구분 코드
[Required(ErrorMessage = "필수 항목 누락")]
public required DateTime start_date { get; set; }
public DateTime? end_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public required byte day { get; set; } // 수업 요일 비트 (월요일부터 가장 좌측 비트)
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(4)]
public required string start_time { get; set; } // 수업 시작 시간
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(4)]
public required string end_time { get; set; } // 수업 종료 시간
}
[Table("class_map")]
public class Class_Map
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(10)]
public required string cid { get; set; } // 강의 구분 코드
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; } // 학생(유저) 구분 코드
}
[Table("class_attendance")]
public class Class_Attendance
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(10)]
public required string cid { get; set; } // 강의 구분 코드
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public required string uid { get; set; } // 학생(유저) 구분 코드
[Required(ErrorMessage = "필수 항목 누락")]
public required DateTime attendace_date { get; set; } // 출석 일자
[Required(ErrorMessage = "필수 항목 누락")]
public required byte attendance_state { get; set; } // 출석 상태 (0=출석, 1=결석, 2=지각, 3=조퇴, 4=기타)
}

View File

@ -0,0 +1,44 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("log_project")]
public class LogProject
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int lid { get; set; }
public DateTime create_date {get; set;}
public string log { get; set; }
}
[Table("log_push")]
public class LogPush
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int lid { get; set; }
public string bid {get; set;}
public string pid {get; set;}
public DateTime create_date {get; set;}
public string create_uid {get; set;}
public string log { get; set; }
}
[Table("log_user")]
public class LogUser
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int lid { get; set; }
public string uid {get; set;}
public DateTime create_date {get; set;}
public string create_uid {get; set;}
public string log { get; set; }
}
}

View File

@ -0,0 +1,177 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
/*
* iOS Payload
* aps
* 1. alert :
* 1.1. title :
* 1.2. subtitle :
* 1.3. body :
* 2. badge :
* 3. sound : "default"
* 4. content-available : .
* 1 ,
* UI에 .
* 5. mutable-content : 1 Notification Service Extension을 .
* 6. category : .
* 7. thread-id : .
*/
/*
* FCM Payload
* https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?hl=ko&_gl=1*3awp3i*_up*MQ..*_ga*MTMyNDk4ODU5MC4xNzQxMTM1MzI3*_ga_CW55HF8NVT*MTc0MTEzNTMyNy4xLjAuMTc0MTEzNTM4My4wLjAuMA..#Notification
* {
"name": string,
"data": { // 입력 전용으로 임의의 가
string: string,
...
},
"notification": {
object (Notification)
},
"android": {
object (AndroidConfig)
},
"webpush": {
object (WebpushConfig)
},
"apns": {
object (ApnsConfig)
},
"fcm_options": {
object (FcmOptions)
},
// Union field target can be only one of the following:
"token": string,
"topic": string,
"condition": string
// End of list of possible types for union field target.
}
* 1. Notification
* 1.1. title:
* 1.2. body:
* 1.3. image: URL
* 1.4. sound:
* 1.5. click_action: ( )
* 1.6. tag:
* 1.7. color: (16 )
* 1.8. body_loc_key body_loc_args:
* 1.9. title_loc_key title_loc_args:
* 1.10. channel_id: Android Oreo
* 1.11. ticker, sticky, event_time, notification_priority, visibility, notification_count, light_settings, vibrate_timings :
* 2. Data : - UI에 .
* 3.
* 3.1. priority: (high normal)
* 3.2. time_to_live (ttl): ( )
* 3.3. collapse_key: collapse_key를
* 3.4. restricted_package_name: (Android )
*/
[Table("payload")]
public class DBPayload
{
public string bid { get; set; }
public string pid { get; set; }
public string title {get; set;}
public string? subtitle {get; set;}
public string body {get; set;}
public bool alert_yn {get; set;}
public string category {get; set;}
public string? content {get; set;}
}
[Table("push_cabinet")]
public class PushCabinet
{
[Key]
public int id { get; set; }
public string uid { get; set; }
public string pid { get; set; }
public string bid { get; set; }
public DateTime send_date { get; set; }
public bool check_yn { get; set; }
public string? content {get; set;}
}
public class PushRequest
{
public string bid { get; set; }
public List<string> uids { get; set; }
public string pid { get; set; }
public string? content { get; set; }
}
public class Payload
{
public Aps aps { get; set; }
public string pid { get; set; }
public string bid { get; set; }
public string content { get; set; }
// public string customKey { get; set; } 이런식으로 추가도 가능
public string ToJson()
{
return System.Text.Json.JsonSerializer.Serialize(this);
}
}
public class Aps
{
public Aps()
{
sound = "default";
content_available = 1;
}
[Required(ErrorMessage = "필수 입력 누락 (alert)")]
public Alert alert { get; set; }
[Required(ErrorMessage = "필수 입력 누락 (badge")]
public int badge { get; set; } // 앱 아이콘 표시 배지 숫자 설정
public string sound { get; set; } // 사운드 파일 이름 default = "default"
public int content_available { get; set; } // 백그라운드 알림 활성화: 필수 (1)
public string? category { get; set; } // 알림에 대한 특정 액션을 정의
}
public class Alert
{
[Required(ErrorMessage = "필수 입력 누락 (title")]
public string title { get; set; } // 제목
[Required(ErrorMessage = "필수 입력 누락 (body)")]
public string body { get; set; } // 내용
public string? subtitle { get; set; } // 부제목 (선택)
}
/// <summary>
/// 푸시 등록하기 위한 apns 여러 데이터 목록
/// </summary>
public class PushFileSetting
{
public string uri { get; set; }
public string p12Path { get; set; }
public string p12PWPath { get; set; }
public string apnsTopic { get; set; }
}
public class PushData
{
public string pushToken { get; set; }
public Payload payload { get; set; }
}
public class CreatePush
{
public string bid { get; set; }
public string title { get; set; }
public string? subtitle { get; set; }
public string body { get; set; }
public bool alert_yn { get; set; } = true;
public string category { get; set; }
public string? content { get; set; }
}
}

View File

@ -0,0 +1,149 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("login")]
public class Login
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(100)]
public string sns_id {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(70)]
public string uid {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
[MaxLength(4)]
public string sns_type {get; set;}
}
[Table("user_academy")]
public class User_Academy
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string bid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public DateTime register_date { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public bool status { get; set; }
}
[Table("user")]
public class User
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string name { get; set; }
public DateTime? birth { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string type { get; set; }
public string? device_id { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public bool auto_login_yn { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public DateTime login_date { get; set; }
public string? push_token { get; set; }
}
[Table("permission")]
public class Permission
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
public bool location_yn {get; set;}
public bool camera_yn {get; set;}
public bool photo_yn {get; set;}
public bool push_yn {get; set;}
public bool market_app_yn {get; set;}
public bool market_sms_yn {get; set;}
public bool market_email_yn {get; set;}
}
[Table("location")]
public class Location
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string lat { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string lng { get; set; }
}
[Table("contact")]
public class Contact
{
[Key]
[Required(ErrorMessage = "필수 항목 누락")]
public string uid { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string email { get; set; }
public string? phone { get; set; }
public string? address { get; set; }
}
// -- -- -- -- -- DB 테이블 -- -- -- -- -- //
public class UserAll
{
[Required(ErrorMessage = "필수 항목 누락")]
public string name { get; set; }
public DateTime? birth { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string type { get; set; }
public string? device_id { get; set; }
public bool auto_login_yn { get; set; }
public DateTime login_date { get; set; }
public string? push_token { get; set; }
[Required(ErrorMessage = "필수 항목 누락")]
public string email { get; set; }
public string? phone { get; set; }
public string? address { get; set; }
public bool location_yn {get; set;}
public bool camera_yn {get; set;}
public bool photo_yn {get; set;}
public bool push_yn {get; set;}
public bool market_app_yn {get; set;}
public bool market_sms_yn {get; set;}
public bool market_email_yn {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
public string sns_id {get; set;}
[Required(ErrorMessage = "필수 항목 누락")]
public string sns_type {get; set;}
}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Back.Program.Models.Entities
{
[Table("version")]
public class Version
{
[Key]
[MaxLength(4)]
public string os_type { get; set; }
[MaxLength(8)]
public string final_ver { get; set; }
[MaxLength(8)]
public string dev_ver { get; set; }
[MaxLength(8)]
public string force_ver { get; set; }
public bool choice_update_yn { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Version = Back.Program.Models.Entities.Version;
namespace Back.Program.Repositories.V1;
public class AppRepository: IAppRepository
{
private readonly AppDbContext _context;
public AppRepository(AppDbContext context)
{
_context = context;
}
public async Task<APIHeader?> FindHeader(string specific)
{
return await _context.APIHeader.FirstOrDefaultAsync(h => h.specific_id == specific);
}
public async Task<Version?> FindVersion(string type)
{
return await _context.Version.FirstOrDefaultAsync(v => v.os_type == (type =="I" ? "VO01" : "VO02"));
}
public async Task<RefreshToken?> FindRefreshToken(string refresh)
{
return await _context.RefreshToken.FirstOrDefaultAsync(r => r.refresh_token == refresh);
}
}

View File

@ -0,0 +1,25 @@
using Back.Program.Common.Data;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Repositories.V1;
public class ClassRepository(AppDbContext context): IClassRepository
{
public async Task<Class_Info?> FindClassInfo(string cid)
{
return await context.Class_Info.FirstOrDefaultAsync(c => c.id == cid);
}
//return _context.Login.FirstOrDefaultAsync(l => l.sns_type == accType && l.sns_id == snsId);
public async Task<Class_Attendance?> FindClassAttendance(string cid, string uid)
{
throw new NotImplementedException();
}
public async Task<Class_Map?> FindClassMap(string cid)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,11 @@
using Back.Program.Common.Model;
using Version = Back.Program.Models.Entities.Version;
namespace Back.Program.Repositories.V1.Interfaces;
public interface IAppRepository
{
Task<APIHeader?> FindHeader(string specific);
Task<Version?> FindVersion(string type);
Task<RefreshToken?> FindRefreshToken(string refresh);
}

View File

@ -0,0 +1,19 @@
using Back.Program.Models.Entities;
namespace Back.Program.Repositories.V1.Interfaces;
public interface IClassRepository
{
// 클래스 정보
// Task<Class_Info> GetClassInfo(string cid);
// //학생이 클래스에 참여했는지 여부
// Task<List<string>> GetIncludeStudents(string cid);
// // 학생이 클래스에서 했던 모든 출석
// Task<List<(DateTime date, int state)>> GetAttendanceOfClass(string cid, string uid);
// // 학생이 특정 날짜에 했던 출석
// Task<List<(string cid, int state)>> GetAttendanceByDate(string uid, DateTime date);
Task<Class_Info?> FindClassInfo(string cid);
Task<Class_Attendance?> FindClassAttendance(string cid, string uid);
Task<Class_Map?> FindClassMap(string cid);
}

View File

@ -0,0 +1,12 @@
using Back.Program.Models.Entities;
namespace Back.Program.Repositories.V1.Interfaces;
public interface ILogRepository
{
Task<bool> SaveLogUser(LogUser log);
Task<bool> SaveLogProject(LogProject log);
Task<bool> SaveLogPush(LogPush log);
}

View File

@ -0,0 +1,18 @@
using Back.Program.Models.Entities;
namespace Back.Program.Repositories.V1.Interfaces;
public interface IPushRepository
{
Task<bool> FindAcademy(string bid);
Task<List<DBPayload>> FindPushList(string bid, string? pid, string? category);
Task<DBPayload?> FindPushPayload(string bid, string pid);
Task<bool> FindUserAcademy(string uid, string bid);
Task<int> CountBadge(string uid);
Task<string?> FindPushToken(string uid);
Task<PushCabinet?> FindPushCabinet(int id);
Task<List<PushCabinet>> FindPushCabinet(string uid, int size);
Task<List<PushCabinet>> FindPushCabinet(int id, int size);
}

View File

@ -0,0 +1,14 @@
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
namespace Back.Program.Repositories.V1.Interfaces
{
public interface IUserRepository
{
Task<Login?> FindLogin(string accType, string snsId);
Task<User?> FindUser(string uid);
Task<RefreshToken?> FindRefreshToken(string uid);
Task<List<AcademyName>> FindAcademies(string uid);
Task<bool> SaveChanges();
}
}

View File

@ -0,0 +1,36 @@
using Back.Program.Common.Data;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
namespace Back.Program.Repositories.V1;
public class LogRepository: ILogRepository
{
private readonly ILogger<LogRepository> _logger;
private readonly AppDbContext _context;
public LogRepository(ILogger<LogRepository> logger, AppDbContext context)
{
_logger = logger;
_context = context;
}
public async Task<bool> SaveLogUser(LogUser log)
{
_context.LogUser.Add(log);
return await _context.SaveChangesAsync() > 0;
}
public async Task<bool> SaveLogProject(LogProject log)
{
_context.LogProject.Add(log);
return await _context.SaveChangesAsync() > 0;
}
public async Task<bool> SaveLogPush(LogPush log)
{
_context.LogPush.Add(log);
return await _context.SaveChangesAsync() > 0;
}
}

View File

@ -0,0 +1,85 @@
using Back.Program.Common.Data;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Repositories.V1;
public class PushRepository: IPushRepository
{
private readonly AppDbContext _context;
public PushRepository(AppDbContext context)
{
_context = context;
}
public async Task<bool> FindAcademy(string bid)
{
return await _context.Academy.AnyAsync(a => a.bid == bid);
}
public async Task<List<DBPayload>> FindPushList(string bid, string? pid, string? category)
{
var pushQuery = _context.DBPayload.Where(p => p.bid == bid);
if (pid != null)
pushQuery = pushQuery.Where(p => p.pid == pid);
if (category != null)
pushQuery = pushQuery.Where(p=>p.category == category);
return await pushQuery.ToListAsync();
}
public async Task<DBPayload?> FindPushPayload(string bid, string pid)
{
return await _context.DBPayload.FirstOrDefaultAsync(p => p.bid == bid && p.pid == pid);
}
public async Task<bool> FindUserAcademy(string uid, string bid)
{
return await _context.UserAcademy.AnyAsync(ua => ua.uid == uid && ua.bid == bid);
}
public async Task<int> CountBadge(string uid)
{
return await _context.PushCabinet.CountAsync(c => c.uid == uid && c.check_yn == false);
}
public async Task<string?> FindPushToken(string uid)
{
return await _context.User
.Where(u => u.uid == uid)
.Select(u => u.push_token)
.FirstOrDefaultAsync();
}
public async Task<PushCabinet?> FindPushCabinet(int id)
{
return await _context.PushCabinet.FirstOrDefaultAsync(c => c.id == id);
}
public async Task<List<PushCabinet>> FindPushCabinet(string uid, int size)
{
return await _context.PushCabinet.Where(c => c.uid == uid)
.OrderBy(c => c.send_date)
.Take(size)
.ToListAsync();
}
public async Task<List<PushCabinet>> FindPushCabinet(int id, int size)
{
var sort = await _context.PushCabinet
.Where(p=> p.id == id)
.Select(p => p.send_date)
.FirstOrDefaultAsync();
if (sort == default) return new List<PushCabinet>();
return await _context.PushCabinet
.Where(c => c.send_date > sort)
.OrderBy(c => c.send_date)
.Take(size)
.ToListAsync();
}
}

View File

@ -0,0 +1,49 @@
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Repositories.V1
{
public class UserRepository: IUserRepository
{
private readonly ILogger<UserRepository> _logger;
private readonly AppDbContext _context;
public UserRepository(ILogger<UserRepository> logger,AppDbContext context) {
_logger = logger;
_context = context;
}
public Task<Login?> FindLogin(string accType, string snsId)
{
return _context.Login.FirstOrDefaultAsync(l => l.sns_type == accType && l.sns_id == snsId);
}
public Task<User?> FindUser(string uid)
{
return _context.User.FirstOrDefaultAsync(u => u.uid == uid);
}
public Task<RefreshToken?> FindRefreshToken(string uid)
{
return _context.RefreshToken.FirstOrDefaultAsync(r => r.uid == uid);
}
public Task<List<AcademyName>> FindAcademies(string uid)
{
var academyList = _context.UserAcademy
.Join(_context.Academy, ua => ua.bid, a => a.bid, (ua, a) => new { ua, a })
.Where(s => s.ua.uid == uid)
.Select(s => new AcademyName { bid = s.a.bid, name = s.a.business_name })
.ToListAsync();
return academyList;
}
public async Task<bool> SaveChanges()
{
return await _context.SaveChangesAsync() > 0;
}
}
}

View File

@ -0,0 +1,168 @@
using System.Security.Cryptography;
using System.Text;
using Back.Program.Common.Auth;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1.Interfaces;
using Version = Back.Program.Models.Entities.Version;
namespace Back.Program.Services.V1;
public class AppService: IAppService
{
private readonly ILogger<IAppService> _logger;
private readonly IRepositoryService _repositoryService;
private readonly JwtTokenService _jwtTokenService;
private readonly ILogRepository _logRepository;
private readonly IAppRepository _appRepository;
public AppService(ILogger<IAppService> logger, IRepositoryService repositoryService,
JwtTokenService jwtTokenService, ILogRepository logRepository, IAppRepository appRepository)
{
_logger = logger;
_repositoryService = repositoryService;
_jwtTokenService = jwtTokenService;
_logRepository = logRepository;
_appRepository = appRepository;
}
public async Task<APIResponseStatus<object>> GetHeader(string summary, string type, string specific, string project)
{
bool valid = false;
switch (type)
{
case "I":
if (project == "me.myds.ipstein.acamate.AcaMate") valid = true;
break;
case "A":
break;
case "W":
if (project == "AcaMate") valid = true;
break;
default:
return APIResponse.InvalidInputError($"[{summary}], 타입 에러");
break;
}
if (valid)
{
var apiHeader = await _appRepository.FindHeader(specific);
string nowTime = DateTime.Now.ToString("o");
string combineText = $"{project}_{nowTime}_{specific}";
string headerValue = KeyGenerator(combineText);
if (apiHeader != null)
{
if (DateTime.Now - apiHeader.connect_date > TimeSpan.FromHours(24))
{
_logger.LogInformation($"[{summary}] : 해당 키 유효기간 경과");
apiHeader.h_value = headerValue;
apiHeader.connect_date = DateTime.Now;
if (await _repositoryService.SaveData<APIHeader>(apiHeader))
{
// 새로 업뎃해서 저장
if(await _logRepository.SaveLogProject( new LogProject { create_date = DateTime.Now , log = $"[{summary}] : 해당 키 유효시간 만료로 인한 새 키 부여"}))
{
_logger.LogInformation($"[{summary}] : 성공");
}
else
{
_logger.LogInformation($"[{summary}] : 성공 - 로그 저장 실패 ");
}
return APIResponse.Send<object>("000", $"[{summary}], 정상", new { header = headerValue });
}
else
{
// 저장이 안되었다? == 서버 오류
return APIResponse.InternalSeverError();
}
}
else
{
return APIResponse.Send<object>("000", $"[{summary}], 정상", new { header = apiHeader.h_value });
// 유효기간 경과도 없고 정상적으로 잘 불러짐
}
}
else
{
// API 저장된 거 없으니 저장 합시다.
var newHeader = new APIHeader
{
h_key = type == "I" ? "iOS_AM_Connect_Key"
: (type == "A" ? "And_AM_Connect_Key"
: (type == "W" ? "Web-AM-Connect-Key": throw new Exception("ERROR"))),
h_value = headerValue,
connect_date = DateTime.Now,
specific_id = specific
};
if (await _repositoryService.SaveData<APIHeader>(newHeader))
{
// 새로 업뎃해서 저장
if(await _logRepository.SaveLogProject( new LogProject { create_date = DateTime.Now , log = $"[{summary}] : 새 기기 등록으로 인한 새 키 부여"}))
{
_logger.LogInformation($"[{summary}] : 성공");
}
else
{
_logger.LogInformation($"[{summary}] : 성공 - 로그 저장 실패 ");
}
return APIResponse.Send<object>("000", $"[{summary}], 정상", new { header = headerValue });
}
else
{
// 저장이 안되었다? == 서버 오류
return APIResponse.InternalSeverError();
}
}
}
else
{
// 헤더 없단 소리 == 문제 있음
return APIResponse.InvalidInputError();
}
}
private string KeyGenerator(string combineText)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combineText));
return BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLowerInvariant();
}
}
public async Task<APIResponseStatus<object>> GetVersion(string summary, string type)
{
var version = await _appRepository.FindVersion(type);
if (version != null)
{
var sendVersion = new Version {
os_type = (version.os_type == "VO01" ? "I" : (version.os_type == "VO02" ? "A" : "W")),
final_ver = version.final_ver,
force_ver = version.force_ver,
dev_ver = version.dev_ver,
choice_update_yn = version.choice_update_yn
};
return APIResponse.Send<object>("000", $"[{summary}], 정상", sendVersion);
}
else
{
return APIResponse.NotFoundError();
}
}
public async Task<APIResponseStatus<object>> RetryAccess(string summary, string refresh)
{
var refreshToken = await _appRepository.FindRefreshToken(refresh);
if (refreshToken == null) return APIResponse.InvalidInputError($"[{summary}] : 리프레시 토큰 문제");
if (refreshToken.revoke_Date != null) return APIResponse.InvalidInputError($"[{summary}] : 리프레시 토큰 폐기");
if (refreshToken.expire_date < DateTime.Now) return APIResponse.InvalidInputError($"[{summary}] : 리프레시 토큰 만료");
string access = _jwtTokenService.GenerateJwtToken(refreshToken.uid);
return APIResponse.Send<object>("000", $"[{summary}], 토큰 생성 완료", new { access = access });
}
}

View File

@ -0,0 +1,8 @@
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Services.V1;
public class ChatService: IChatService
{
}

View File

@ -0,0 +1,21 @@
using Back.Program.Common.Model;
using Back.Program.Repositories.V1;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Services.V1;
public class ClassService(IClassRepository classRepository): IClassService
{
public async Task<APIResponseStatus<object>> GetClassInfo(string summary, string cid)
{
var data = await classRepository.FindClassInfo(cid);
return APIResponse.Send<object>("000", "수업 정보 조회 성공", new
{
summary = summary,
data = data
}
);
}
}

View File

@ -0,0 +1,66 @@
using System.Collections.Concurrent;
using Back.Program.Models.Entities;
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Services.V1
{
public interface IPushQueue
{
void Enqueue(PushData pushData);
Task<PushData> DequeueAsync(CancellationToken cancellationToken);
}
public class InMemoryPushQueue: IPushQueue
{
private readonly ConcurrentQueue<PushData> _queue = new ConcurrentQueue<PushData>();
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
public void Enqueue(PushData pushData)
{
if( pushData is null )
throw new ArgumentNullException(nameof(pushData));
_queue.Enqueue(pushData);
_signal.Release();
}
public async Task<PushData> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_queue.TryDequeue(out var pushData);
return pushData;
}
}
public class PushBackgroundService : BackgroundService
{
private readonly IPushQueue _queue;
private readonly IServiceScopeFactory _scopeFactory;
public PushBackgroundService(IPushQueue queue, IServiceScopeFactory scopeFactory)
{
_queue = queue;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var pushData = await _queue.DequeueAsync(stoppingToken);
using var scope = _scopeFactory.CreateScope();
var pushService = scope.ServiceProvider.GetRequiredService<IPushService>();
try
{
await pushService.SendPushNotificationAsync(pushData.pushToken, pushData.payload);
}
catch (Exception ex)
{
Console.WriteLine($"푸시 전송 실패: {ex.Message}");
}
}
}
}
}

View File

@ -0,0 +1,11 @@
using Back.Program.Common.Model;
namespace Back.Program.Services.V1.Interfaces;
public interface IAppService
{
Task<APIResponseStatus<object>> GetHeader(string summary, string type, string specific, string project);
Task<APIResponseStatus<object>> GetVersion(string summary, string type);
Task<APIResponseStatus<object>> RetryAccess(string summary, string refresh);
}

View File

@ -0,0 +1,6 @@
namespace Back.Program.Services.V1.Interfaces;
public interface IChatService
{
}

View File

@ -0,0 +1,8 @@
using Back.Program.Common.Model;
namespace Back.Program.Services.V1.Interfaces;
public interface IClassService
{
Task<APIResponseStatus<object>> GetClassInfo(string summary, string cid);
}

View File

@ -0,0 +1,13 @@
using Back.Program.Common.Model;
namespace Back.Program.Services.V1.Interfaces;
public interface IKakaoService
{
Task<string> GetAccessToken(string code);
Task<string> GetAuthorizationUrl(string scope);
Task<(bool Success, string Response)> Redirect(string code);
Task<(bool Success, string Response)> Logout(string accessToken);
Task<(bool Success, string Response)> Unlink(string accessToken);
Task<(bool Success, string Response)> UserMe(string accessToken);
}

View File

@ -0,0 +1,19 @@
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
namespace Back.Program.Services.V1.Interfaces;
public interface IPushService
{
Task SendPushNotificationAsync(string deviceToken, Payload payload);
Task<APIResponseStatus<object>> GetPush(string summary, string bid, string? pid, string? category);
Task<APIResponseStatus<object>> SendPush(string summary, PushRequest pushRequest);
Task<APIResponseStatus<object>> SetPush(string summary, string token, DBPayload request);
Task<APIResponseStatus<object>> CreatePush(string summary, string token, CreatePush request);
Task<APIResponseStatus<object>> DeletePush(string summary, string token, string bid, string pid);
Task<APIResponseStatus<object>> DeleteListPush(string summary, string token, int id);
Task<APIResponseStatus<object>> SearchToUserPush(string summary, string token, int size, PushCabinet? request);
}

View File

@ -0,0 +1,12 @@
using System.Linq.Expressions;
namespace Back.Program.Services.V1.Interfaces;
public interface IRepositoryService
{
// Task<ValidateToken> ValidateToken(string token, string refresh);
Task<bool> SaveData<T>(T entity, Expression<Func<T, object>> key = null) where T : class;
Task<bool> DeleteData<T>(T entity, Expression<Func<T, object>> key = null) where T : class;
String ReadSummary(Type type, String name);
Task SendFrontData<T>(T data, string url);
}

View File

@ -0,0 +1,9 @@
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
namespace Back.Program.Services.V1.Interfaces;
public interface ISessionService
{
Task<APIResponseStatus<object>> GetSessionData(string summary);
}

View File

@ -0,0 +1,15 @@
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
namespace Back.Program.Services.V1.Interfaces
{
public interface IUserService
{
Task<APIResponseStatus<object>> GetUser(string summary, string token);
Task<APIResponseStatus<object>> Login(string summary, string accType, string snsId);
Task<APIResponseStatus<object>> Register(string summary, UserAll request);
Task<APIResponseStatus<object>> Logout(string summary, string token);
Task<APIResponseStatus<object>> Cancel(string summary, string token);
Task<APIResponseStatus<object>> GetAcademy(string summary, string token);
}
}

View File

@ -0,0 +1,157 @@
using System.Text.Json;
using System.Net.Http.Headers;
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Services.V1;
public class KakaoService: IKakaoService
{
private readonly HttpClient _httpClient;
private const string KAKAO_API_BASE_URL = "https://kapi.kakao.com";
private const string KAKAO_AUTH_BASE_URL = "https://kauth.kakao.com";
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _redirectUri;
public KakaoService(HttpClient httpClient, IConfiguration configuration)
{
_httpClient = httpClient;
_clientId = configuration["Kakao:ClientId"] ?? throw new InvalidOperationException("Kakao:ClientId not configured");
_redirectUri = configuration["Kakao:RedirectUri"] ?? throw new InvalidOperationException("Kakao:RedirectUri not configured");
_clientSecret = configuration["Kakao:ClientSecret"] ?? throw new InvalidOperationException("Kakao:ClientSecret not configured");
}
private void SetHeaders(string accessToken)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
private async Task<string> Call(HttpMethod method, string url, HttpContent? content = null)
{
try
{
var request = new HttpRequestMessage(method, url);
if (content != null)
{
request.Content = content;
}
var response = await _httpClient.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return JsonSerializer.Serialize(new { error = $"HTTP {(int)response.StatusCode}: {responseContent}" });
}
return responseContent;
}
catch (Exception ex)
{
return JsonSerializer.Serialize(new { error = ex.Message });
}
}
/// <summary>
/// 인가 받은 코드로 토큰 받기를 실행하고 이 토큰으로 사용자 정보 가져오기를 할 수 있다.
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public async Task<string> GetAccessToken(string code)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("client_id", _clientId),
new KeyValuePair<string, string>("redirect_uri", _redirectUri),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("client_secret", _clientSecret)
});
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var response = await Call(HttpMethod.Post, $"{KAKAO_AUTH_BASE_URL}/oauth/token", content);
var responseData = JsonSerializer.Deserialize<JsonElement>(response);
if (responseData.TryGetProperty("error", out var error))
{
return response;
}
if (!responseData.TryGetProperty("access_token", out var accessToken))
{
return JsonSerializer.Serialize(new { error = "Access token is missing from response" });
}
return JsonSerializer.Serialize(new { access_token = accessToken.GetString() });
}
/// <summary>
/// 인가코드를 받는다 이 인가코드를 사용해 토큰 받기를 요청 할 수 있다.
/// 이게 리다이렉트에 포함되어있음
/// </summary>
/// <param name="scope"></param>
/// <returns></returns>
public Task<string> GetAuthorizationUrl(string scope)
{
var authUrl = $"{KAKAO_AUTH_BASE_URL}/oauth/authorize?client_id={_clientId}&redirect_uri={_redirectUri}&response_type=code";
if (!string.IsNullOrEmpty(scope))
{
authUrl += $"&scope={scope}";
}
return Task.FromResult(authUrl);
}
public async Task<(bool Success, string Response)> Redirect(string code)
{
if (string.IsNullOrEmpty(code))
return (false, JsonSerializer.Serialize(new { error = "Authorization code not found" }));
var response = await GetAccessToken(code);
var responseData = JsonSerializer.Deserialize<JsonElement>(response);
if (responseData.TryGetProperty("error", out var error))
{
return (false, response);
}
var accessToken = responseData.GetProperty("access_token").GetString();
if (string.IsNullOrEmpty(accessToken))
{
return (false, response);
}
return (true, accessToken);
}
public async Task<(bool Success, string Response)> Logout(string accessToken)
{
if (string.IsNullOrEmpty(accessToken))
return (false, JsonSerializer.Serialize(new { error = "Not logged in" }));
SetHeaders(accessToken);
var response = await Call(HttpMethod.Post, $"{KAKAO_API_BASE_URL}/v1/user/logout");
return (true, response);
}
public async Task<(bool Success, string Response)> Unlink(string accessToken)
{
if (string.IsNullOrEmpty(accessToken))
return (false, JsonSerializer.Serialize(new { error = "Not logged in" }));
SetHeaders(accessToken);
var response = await Call(HttpMethod.Post, $"{KAKAO_API_BASE_URL}/v1/user/unlink");
return (true, response);
}
public async Task<(bool Success, string Response)> UserMe(string accessToken)
{
if (string.IsNullOrEmpty(accessToken))
return (false, JsonSerializer.Serialize(new { error = "Not logged in" }));
SetHeaders(accessToken);
var response = await Call(HttpMethod.Get, $"{KAKAO_API_BASE_URL}/v2/user/me");
return (true, response);
}
}

View File

@ -0,0 +1,342 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Back.Program.Common.Auth;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1.Interfaces;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
using Polly;
using Version = System.Version;
namespace Back.Program.Services.V1
{
public class PushService : IPushService
{
private readonly ILogger<PushService> _logger;
private readonly HttpClient _httpClient;
private readonly PushFileSetting _setting;
private readonly JwtTokenService _jwtTokenService;
private readonly IRepositoryService _repositoryService;
private readonly IPushQueue _pushQueue;
private readonly ILogRepository _logRepository;
private readonly IPushRepository _pushRepository;
public PushService(ILogger<PushService> logger, HttpClient httpClient, IOptions<PushFileSetting> options,
IPushRepository pushRepository, IRepositoryService repositoryService, IPushQueue pushQueue,
ILogRepository logRepository, JwtTokenService jwtTokenService)
{
_logger = logger;
_httpClient = httpClient;
_setting = options.Value;
_pushRepository = pushRepository;
_repositoryService = repositoryService;
_pushQueue = pushQueue;
_logRepository = logRepository;
_jwtTokenService = jwtTokenService;
}
public async Task SendPushNotificationAsync(string deviceToken, Payload payload)
{
// 존재 안하면 예외 던져 버리는거
if (!File.Exists(_setting.p12Path) || !File.Exists(_setting.p12PWPath))
throw new FileNotFoundException("[푸시] : p12 관련 파일 확인 필요");
var jsonPayload = JsonSerializer.Serialize(payload);
var request = new HttpRequestMessage(HttpMethod.Post, $"/3/device/{deviceToken}")
{
Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"),
Version = new Version(2, 0) // HTTP/2 사용
};
// 필수 헤더 추가
request.Headers.Add("apns-topic", _setting.apnsTopic);
request.Headers.Add("apns-push-type", "alert");
var policy = Policy.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
await policy.ExecuteAsync(async () =>
{
// var response = await _httpClient.SendAsync(request);
try
{
var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
// Console.WriteLine($"[APNs 응답] StatusCode: {response.StatusCode}");
// Console.WriteLine($"[APNs 응답 본문]: {result}");
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new AcaException(ResposeCode.NetworkErr, $"[푸시] : APNS 통신 실패 - {errorContent}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[푸시 전송 예외 발생] {ex.GetType().Name}: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine(
$"[InnerException] {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
}
});
}
public async Task<APIResponseStatus<object>> GetPush(string summary, string bid, string? pid, string? category)
{
if (!(await _pushRepository.FindAcademy(bid)))
return APIResponse.Send<object>("100", $"[{summary}], 존재하지 않는 BID", new {});
var pushData = await _pushRepository.FindPushList(bid, pid, category);
if (pushData.Count > 0)
return APIResponse.Send<object>("000", $"[{summary}, 정상", pushData);
return APIResponse.Send<object>("001", $"[{summary}], PUSH 데이터 없음", new {});
}
public async Task<APIResponseStatus<object>> SendPush(string summary, PushRequest pushRequest)
{
var payload = await _pushRepository.FindPushPayload(pushRequest.bid, pushRequest.pid);
if (payload == null) return APIResponse.InvalidInputError($"[{summary}], 저장된 payload 없음");
var pushTasks = pushRequest.uids.Select(async uid =>
{
if (!await _pushRepository.FindUserAcademy(uid, pushRequest.bid)) return;
var badge = await _pushRepository.CountBadge(uid);
var pushToken = await _pushRepository.FindPushToken(uid);
if (pushToken == null) return;
var newPayload = new Payload
{
aps = new Aps
{
alert = new Alert
{ title = payload.title, body = payload.body, subtitle = payload.subtitle ?? "" },
category = payload.category,
badge = badge + 1
},
pid = pushRequest.pid,
bid = pushRequest.bid,
content = pushRequest.content ?? (payload.content ?? "")
};
var pushCabinet = new PushCabinet
{
uid = uid,
bid = pushRequest.bid,
pid = pushRequest.pid,
send_date = DateTime.Now,
content = newPayload.content != "" ? newPayload.content : null
};
var pushData = new PushData
{
pushToken = pushToken,
payload = newPayload
};
var log = new LogPush
{
bid = pushRequest.bid,
pid = pushRequest.pid,
create_date = DateTime.Now,
create_uid = "System",
log = ""
};
var saved = await _repositoryService.SaveData<PushCabinet>(pushCabinet);
log.log = saved ? $"[{summary}]: 푸시 캐비닛 저장 성공 및 푸시 전송 시도" : $"[{summary}]: 푸시 전송 실패";
var logSaved = await _repositoryService.SaveData<LogPush>(log);
_logger.LogInformation($"[{summary}]: 캐비닛 저장 = {saved} : 로그 저장 = {logSaved}");
if(saved) _pushQueue.Enqueue(pushData);
});
await Task.WhenAll(pushTasks);
return APIResponse.Send<object>("000", $"[{summary}], 정상", new {});
}
public async Task<APIResponseStatus<object>> SetPush(string summary, string token, DBPayload request)
{
string uid = String.Empty;
if (token == "System") uid = "System";
else
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
var payload = await _pushRepository.FindPushPayload(request.bid, request.pid);
if (payload == null)
return APIResponse.Send<object>("100", $"[{summary}], PID, BID 또는 Cabinet 오류", new {});
var log = new LogPush
{
bid = request.bid,
pid = request.pid,
create_date = DateTime.Now,
create_uid = uid
};
if (payload.title != request.title && request.title != "") payload.title = request.title;
if (payload.body != request.body && request.body != "") payload.body = request.body;
if (payload.subtitle != request.subtitle) payload.subtitle = request.subtitle;
if (payload.alert_yn != request.alert_yn) payload.alert_yn = request.alert_yn;
if (payload.category != request.category) payload.category = request.category;
if (request.content != request.content) payload.content = request.content;
var saved = (await _repositoryService.SaveData<DBPayload>(payload));
log.log = $"[{summary}] : 상태 = {saved}";
var logSaved = await _repositoryService.SaveData<LogPush>(log);
_logger.LogInformation($"[{summary}]: 상태 = {saved} : 로그 저장 = {logSaved}");
if (!saved) return APIResponse.Send<object>("001", $"[{summary}], 실패", new {});
return APIResponse.Send<object>("000", $"[{summary}], 정상", new {});
}
public async Task<APIResponseStatus<object>> CreatePush(string summary, string token, CreatePush request)
{
Func<string, int, string> randomLetter = (letters, count) => new string(Enumerable.Range(0, count).Select(_ => letters[new Random().Next(letters.Length)]).ToArray());
var letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var digits = "0123456789";
var frontLetters = $"{randomLetter(letters, 1)}{randomLetter(digits, 1)}{randomLetter(letters, 1)}";
var afterLetters = $"{randomLetter(letters, 1)}{randomLetter(digits, 1)}{randomLetter(letters, 1)}";
string uid = String.Empty;
if (token == "System") uid = "System";
else
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
if (!(await _pushRepository.FindAcademy(request.bid)))
return APIResponse.Send<object>("100", $"[{summary}], 학원 정보(BID) 확인 불가", new {});
DBPayload payload = new DBPayload
{
bid = request.bid,
pid = $"AP{DateTime.Now:yyyyMMdd}{frontLetters}{DateTime.Now:HHmmss}{afterLetters}",
title = request.title,
subtitle = request.subtitle,
body = request.body,
alert_yn = request.alert_yn,
category = request.category,
content = request.content,
};
var log = new LogPush
{
bid = payload.bid,
pid = payload.pid,
create_date = DateTime.Now,
create_uid = uid
};
var saved = await _repositoryService.SaveData<DBPayload>(payload);
log.log = $"[{summary}] : 푸시 생성 = {saved}";
var logSaved = await _repositoryService.SaveData<LogPush>(log);
_logger.LogInformation($"[{summary}]: 푸시 생성 = {saved} : 로그 저장 = {logSaved}");
if (!saved) return APIResponse.Send<object>("001", $"[{summary}], 실패", new {});
return APIResponse.Send<object>("000", $"[{summary}], 정상", new {});
}
public async Task<APIResponseStatus<object>> DeletePush(string summary, string token, string bid, string pid)
{
string uid = String.Empty;
if (token == "System") uid = "System";
else
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
var payload = await _pushRepository.FindPushPayload(bid, pid);
if (payload == null) return APIResponse.Send<object>("001", $"[{summary}], 삭제 할 PUSH 없음", new {});
var log = new LogPush
{
bid = payload.bid,
pid = payload.pid,
create_date = DateTime.Now,
create_uid = uid
};
var delete = await _repositoryService.DeleteData<DBPayload>(payload);
log.log = $"[{summary}] : 삭제 = {delete}";
var logSaved = await _repositoryService.SaveData<LogPush>(log);
_logger.LogInformation($"[{summary}]: 삭제 = {delete} : 로그 저장 = {logSaved}");
if (!delete) return APIResponse.Send<object>("002", $"[{summary}], 실패", new {});
return APIResponse.Send<object>("000", $"[{summary}], 정상", new {});
}
public async Task<APIResponseStatus<object>> DeleteListPush(string summary, string token, int id)
{
string uid = String.Empty;
if (token == "System") uid = "System";
else
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
var cabinet = await _pushRepository.FindPushCabinet(id);
if (cabinet == null) return APIResponse.Send<object>("001", $"[{summary}], 삭제 할 PUSH 없음", new {});
var log = new LogPush
{
bid = cabinet.bid,
pid = cabinet.pid,
create_date = DateTime.Now,
create_uid = uid
};
var delete = await _repositoryService.DeleteData<PushCabinet>(cabinet);
log.log = $"[{summary}] : {cabinet.pid} 삭제 = {delete}";
var logSaved = await _repositoryService.SaveData<LogPush>(log);
_logger.LogInformation($"[{summary}]: {cabinet.pid} 삭제 = {delete} : 로그 저장 = {logSaved}");
if (!delete) return APIResponse.Send<object>("002", $"[{summary}], 실패", new {});
return APIResponse.Send<object>("000", $"[{summary}], 정상", new {});
}
public async Task<APIResponseStatus<object>> SearchToUserPush(string summary, string token, int size, PushCabinet? request)
{
string uid = String.Empty;
if (token == "System") uid = "System";
else
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
if (request == null)
{
var data = await _pushRepository.FindPushCabinet(uid, size);
return APIResponse.Send<object>("000", $"[{summary}], 정상", data);
}
else
{
var data = await _pushRepository.FindPushCabinet(request.id, size);
return APIResponse.Send<object>("000", $"[{summary}], 정상", data);
}
}
}
}

View File

@ -0,0 +1,180 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.Json;
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Services.V1.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace Back.Program.Services.V1
{
public class RepositoryService: IRepositoryService
{
private readonly AppDbContext _dbContext;
private readonly ILogger<RepositoryService> _logger;
private readonly JwtTokenService _jwtTokenService;
public RepositoryService(AppDbContext dbContext, ILogger<RepositoryService> logger, JwtTokenService jwtTokenService)
{
_dbContext = dbContext;
_logger = logger;
_jwtTokenService = jwtTokenService;
}
public async Task<bool> SaveData<T>(T entity, Expression<Func<T, object>> key = null) where T : class
{
try
{
if (key != null)
{
// key를 가지고 EF 로 돌리는게 아니라 내가 조건을 넣어서 하는 경우에 사용함
var value = key.Compile()(entity);
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var entityData = await _dbContext.Set<T>().FirstOrDefaultAsync(predicate);
if (entityData != null)
{
_logger.LogInformation($"[{typeof(T)}] 해당 PK 존재 = [{value}]: 계속");
_dbContext.Entry(entityData).CurrentValues.SetValues(entity);
if (!(_dbContext.Entry(entityData).Properties.Any(p => p.IsModified)))
{
_logger.LogInformation($"[{typeof(T)}] 변경 사항 없음");
return true;
}
_logger.LogInformation($"[{typeof(T)}] 변경 사항이 존재");
}
else
{
_logger.LogInformation($"[{typeof(T)}] 처음등록");
_dbContext.Set<T>().Add(entity);
}
}
else
{
// EF 로 직접 키를 잡아서 사용 (관계키나 이런거 할때도 노상관됨)
// 모델이 존재하지 않거나 기본 키 정의가 되지 않은 오류가 발생할 건데 그건 운영 단계에서는 오류 나면 안되는거니
var keyProperties = _dbContext.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties;
// 각 키 속성에 대해, entity에서 실제 키 값을 추출
var keyValues = keyProperties.Select(p => typeof(T).GetProperty(p.Name).GetValue(entity)).ToArray();
// 기본 키 값을 이용해서 기존 엔티티를 찾음 (복합 키도 자동으로 처리됨)
var existingEntity = await _dbContext.Set<T>().FindAsync(keyValues);
if (existingEntity != null)
{
_logger.LogInformation($"[{typeof(T)}] 기존 데이터 발견: 기본 키 값({string.Join(", ", keyValues)})");
// 기존 엔티티를 업데이트: 새 entity의 값으로 교체
_dbContext.Entry(existingEntity).CurrentValues.SetValues(entity);
}
else
{
_logger.LogInformation($"[{typeof(T)}] 신규 데이터 등록: 기본 키 값({string.Join(", ", keyValues)})");
// 데이터가 없으면 새 엔티티 추가
_dbContext.Set<T>().Add(entity);
}
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB 저장 완료: 종료");
return true;
}
catch (Exception ex)
{
_logger.LogInformation($"[{typeof(T)}] 저장 중 알 수 없는 오류 발생: {ex}");
return false;
}
}
public async Task<bool> DeleteData<T>(T entity, Expression<Func<T, object>> key = null) where T : class
{
try
{
if (key != null)
{
// key를 통해 조건식을 만들어 삭제할 엔티티를 찾는 경우
var value = key.Compile()(entity);
var parameter = Expression.Parameter(typeof(T), "x");
var invokedExpr = Expression.Invoke(key, parameter);
var constantExpr = Expression.Constant(value, key.Body.Type);
var equalsExpr = Expression.Equal(invokedExpr, constantExpr);
var predicate = Expression.Lambda<Func<T, bool>>(equalsExpr, parameter);
var entityData = await _dbContext.Set<T>().FirstOrDefaultAsync(predicate);
if (entityData == null)
{
_logger.LogInformation($"[{typeof(T)}] 삭제 대상 데이터가 존재하지 않습니다. (값 = {value})");
return false;
}
_logger.LogInformation($"[{typeof(T)}] 조건에 맞는 데이터 발견 (값 = {value}): 삭제 진행");
_dbContext.Set<T>().Remove(entityData);
}
else
{
// key가 없는 경우 EF Core 메타데이터를 사용하여 기본 키를 통한 삭제
var entityType = _dbContext.Model.FindEntityType(typeof(T));
if (entityType == null)
{
throw new InvalidOperationException($"Entity type '{typeof(T).Name}'이 모델에 존재하지 않습니다.");
}
var primaryKey = entityType.FindPrimaryKey();
if (primaryKey == null)
{
throw new InvalidOperationException($"Entity type '{typeof(T).Name}'에 기본 키가 정의되어 있지 않습니다.");
}
var keyProperties = primaryKey.Properties;
var keyValues = keyProperties.Select(p => typeof(T).GetProperty(p.Name).GetValue(entity)).ToArray();
var existingEntity = await _dbContext.Set<T>().FindAsync(keyValues);
if (existingEntity == null)
{
_logger.LogInformation($"[{typeof(T)}] 기본 키 값({string.Join(", ", keyValues)})에 해당하는 삭제 대상 데이터가 존재하지 않습니다.");
return false;
}
_logger.LogInformation($"[{typeof(T)}] 기본 키 값({string.Join(", ", keyValues)})에 해당하는 데이터 발견: 삭제 진행");
_dbContext.Set<T>().Remove(existingEntity);
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"[{typeof(T)}] DB에서 삭제 완료");
return true;
}
catch (Exception ex)
{
_logger.LogError($"[{typeof(T)}] 삭제 중 오류 발생: {ex}");
return false;
}
}
public string ReadSummary(Type type, string name)
{
var method = type.GetMethod(name) ?? throw new AcaException(ResposeCode.NetworkErr,"swagger summary Load ERROR: NULL");
var att = method.GetCustomAttribute<CustomOperationAttribute>() ?? throw new AcaException(ResposeCode.NetworkErr,"swagger summary Load ERROR: NULL");
return att.Summary;
}
public async Task SendFrontData<T>(T data, string url)
{
using var httpClient = new HttpClient();
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(url, content);
response.EnsureSuccessStatusCode();
}
}
}

View File

@ -0,0 +1,111 @@
using System.Security.Claims;
using System.Text.Json;
using Back.Program.Common.Auth;
using Back.Program.Common.Data;
using Back.Program.Common.Model;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1.Interfaces;
namespace Back.Program.Services.V1;
public class SessionService: ISessionService
{
private readonly ILogger<ISessionService> _logger;
private readonly IUserRepository _userRepository;
private readonly JwtTokenService _jwtTokenService;
private readonly IRepositoryService _repositoryService;
private readonly ILogRepository _logRepository;
private readonly SessionManager _sessionManager;
private readonly IAppService _appService;
public SessionService(ILogger<ISessionService> logger, IUserRepository userRepository,
JwtTokenService jwtTokenService,
IRepositoryService repositoryService, ILogRepository logRepository,
SessionManager sessionManager, IAppService appService)
{
_logger = logger;
_userRepository = userRepository;
_jwtTokenService = jwtTokenService;
_repositoryService = repositoryService;
_logRepository = logRepository;
_sessionManager = sessionManager;
_appService = appService;
}
public async Task<APIResponseStatus<object>> GetSessionData(string summary)
{
try
{
_logger.LogInformation($"[{summary}] 세션 데이터 조회 시작");
// 1. 세션에서 토큰 가져오기
var (result, token) = await _sessionManager.GetString("token");
_logger.LogInformation($"[{summary}] 세션에서 토큰 가져오기 결과: {result}, 토큰: {token}");
if (!result || string.IsNullOrEmpty(token))
{
_logger.LogWarning($"[{summary}] 세션에 토큰이 없습니다");
return APIResponse.Send<object>("200", "세션에 토큰이 없습니다", new { });
}
// 2. 토큰 검증
var validToken = await _jwtTokenService.ValidateToken(token);
_logger.LogInformation($"[{summary}] 토큰 검증 결과: {validToken != null}");
if (validToken == null)
{
// 3. 토큰이 유효하지 않으면 리프레시 토큰으로 새 토큰 발급 시도
var (refreshResult, refreshToken) = await _sessionManager.GetString("refresh");
_logger.LogInformation($"[{summary}] 리프레시 토큰 가져오기 결과: {refreshResult}, 토큰: {refreshToken}");
if (!refreshResult || string.IsNullOrEmpty(refreshToken))
{
_logger.LogWarning($"[{summary}] 리프레시 토큰이 없습니다");
return APIResponse.Send<object>("201", "리프레시 토큰이 없습니다", new { });
}
// 4. 리프레시 토큰으로 새 토큰 발급
var retryResult = await _appService.RetryAccess(summary, refreshToken);
_logger.LogInformation($"[{summary}] 토큰 재발급 결과: {retryResult.status.code}");
if (retryResult.status.code == "000")
{
// 5. 새 토큰을 세션에 저장
var data = JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(retryResult.data));
var newToken = data.GetProperty("access").GetString();
await _sessionManager.SetString("token", newToken);
_logger.LogInformation($"[{summary}] 새 토큰 세션 저장 완료");
// 6. 새 토큰으로 사용자 정보 조회
validToken = await _jwtTokenService.ValidateToken(newToken);
}
else
{
_logger.LogWarning($"[{summary}] 토큰 갱신 실패: {retryResult.status.message}");
return APIResponse.Send<object>("202", "토큰 갱신 실패", new { });
}
}
// 7. 최종적으로 유효한 토큰으로 사용자 정보 조회
var uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
_logger.LogInformation($"[{summary}] 사용자 ID: {uid}");
var user = await _userRepository.FindUser(uid);
_logger.LogInformation($"[{summary}] 사용자 정보 조회 결과: {user != null}");
if (user == null)
{
_logger.LogWarning($"[{summary}] 사용자 정보를 찾을 수 없습니다");
return APIResponse.Send<object>("203", "사용자 정보를 찾을 수 없습니다", new { });
}
_logger.LogInformation($"[{summary}] 세션 데이터 조회 성공: {user.name}");
return APIResponse.Send<object>("000", $"[{summary}], 정상", user);
}
catch (Exception ex)
{
_logger.LogError($"[{summary}] 세션 데이터 조회 중 오류: {ex.Message}");
_logger.LogError($"[{summary}] 스택 트레이스: {ex.StackTrace}");
return APIResponse.InternalSeverError($"[{summary}], 세션 데이터 조회 실패");
}
}
}

View File

@ -0,0 +1,244 @@
using System.Security.Claims;
using Back.Program.Common.Auth;
using Back.Program.Common.Model;
using Back.Program.Models.Entities;
using Back.Program.Repositories.V1.Interfaces;
using Back.Program.Services.V1.Interfaces;
using System.Text.Json;
using Back.Program.Common.Data;
namespace Back.Program.Services.V1
{
public class UserService : IUserService
{
private readonly ILogger<IUserService> _logger;
private readonly IUserRepository _userRepository;
private readonly JwtTokenService _jwtTokenService;
private readonly IRepositoryService _repositoryService;
private readonly ILogRepository _logRepository;
private readonly IAppService _appService;
private readonly SessionManager _sessionManager;
public UserService(ILogger<IUserService> logger, IUserRepository userRepository,
JwtTokenService jwtTokenService,
IRepositoryService repositoryService, ILogRepository logRepository,
IAppService appService, SessionManager sessionManager)
{
_logger = logger;
_userRepository = userRepository;
_jwtTokenService = jwtTokenService;
_repositoryService = repositoryService;
_logRepository = logRepository;
_appService = appService;
_sessionManager = sessionManager;
}
public async Task<APIResponseStatus<object>> GetUser(string summary, string token)
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
var uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var user = await _userRepository.FindUser(uid);
return APIResponse.Send<object>("000", $"[{summary}], 정상", user);
// user 없는 경우가 없네? 그거도 만들것
}
public async Task<APIResponseStatus<object>> Login(string summary, string accType, string snsId)
{
var login = await _userRepository.FindLogin(accType, snsId);
if (login == null)
return APIResponse.Send<object>("001", $"[{summary}], 로그인 정보 없음", new { });
var user = await _userRepository.FindUser(login.uid);
if (user == null)
return APIResponse.Send<object>("002", $"[{summary}], 회원 정보 오류", new { });
user.login_date = DateTime.Now;
var token = _jwtTokenService.GenerateJwtToken(user.uid);
var refresh = _jwtTokenService.GenerateRefreshToken(user.uid);
if (await _repositoryService.SaveData<RefreshToken>(refresh))
{
return APIResponse.Send<object>("000", $"[{summary}], 정상",
new { token = token, refresh = refresh.refresh_token });
}
// 토큰 저장에 실패 및 로그인도 실패
return APIResponse.InternalSeverError($"[{summary}], 로그인 동작 실패");
}
public async Task<APIResponseStatus<object>> Register(string summary, UserAll request)
{
var localPartEmail = request.email.Substring(0, request.email.IndexOf('@'));
var uid = $"AM{localPartEmail}{DateTime.Now:yyyyMMdd}";
var user = new User
{
uid = uid,
name = request.name,
birth = request.birth,
type = request.type,
device_id = request.device_id,
auto_login_yn = request.auto_login_yn,
login_date = request.login_date,
push_token = request.push_token
};
var login = new Login
{
uid = uid,
sns_id = request.sns_id,
sns_type = request.sns_type
};
var permission = new Permission
{
uid = uid,
location_yn = request.location_yn,
camera_yn = request.camera_yn,
photo_yn = request.photo_yn,
push_yn = request.push_yn,
market_app_yn = request.market_app_yn,
market_sms_yn = request.market_sms_yn,
market_email_yn = request.market_email_yn
};
var contact = new Contact
{
uid = uid,
email = request.email,
phone = request.phone,
address = request.address
};
var logUser = new LogUser
{
uid = login.uid,
create_date = DateTime.Now,
create_uid = "System",
log = ""
};
var saveUser = await _repositoryService.SaveData<User>(user);
var saveLogin = await _repositoryService.SaveData<Login>(login);
var savePermission = await _repositoryService.SaveData<Permission>(permission);
var saveContact = await _repositoryService.SaveData<Contact>(contact);
if (saveUser && saveLogin && savePermission && saveContact)
{
var token = _jwtTokenService.GenerateJwtToken(uid);
var refresh = _jwtTokenService.GenerateRefreshToken(uid);
if (await _repositoryService.SaveData<RefreshToken>(refresh))
{
logUser.log = $"[{summary}] : 정상";
if (await _logRepository.SaveLogUser(logUser))
{
_logger.LogInformation($"[{summary}]: 성공");
}
else
{
_logger.LogInformation($"[{summary}]: 성공 - 로그 저장 실패");
}
return APIResponse.Send<object>("000", $"[{summary}], 정상", new
{
token = token,
refresh = refresh.refresh_token
});
}
else
{
logUser.log = $"[{summary}] : 실패";
if (await _logRepository.SaveLogUser(logUser))
{
_logger.LogInformation($"[{summary}]: 실패");
}
else
{
_logger.LogInformation($"[{summary}]: 실패 - 로그 저장 실패");
}
}
}
return APIResponse.InternalSeverError($"[{summary}], 회원가입 동작 실패");
}
public async Task<APIResponseStatus<object>> Logout(string summary, string token)
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
var uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var refresh = await _userRepository.FindRefreshToken(uid);
if (refresh != null)
{
refresh.revoke_Date = DateTime.Now;
if (await _repositoryService.SaveData<RefreshToken>(refresh))
{
return APIResponse.Send<object>("000", $"[{summary}], 로그아웃 정상", new { });
}
}
return APIResponse.UnknownError();
}
public async Task<APIResponseStatus<object>> Cancel(string summary, string token)
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
var uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var user = await _userRepository.FindUser(uid);
if (user == null) return APIResponse.Send<object>("001", $"[{summary}], 회원 정보 확인 오류", new { });
if (await _repositoryService.DeleteData<User>(user))
{
if (await _logRepository.SaveLogUser(new LogUser
{
uid = user.uid, create_date = DateTime.Now, create_uid = "System", log = $"[{summary}] : 정상"
}))
{
_logger.LogInformation($"[{summary}]: 성공");
}
else
{
_logger.LogInformation($"[{summary}]: 성공 - 로그 저장 실패");
}
return APIResponse.Send<object>("000", $"[{summary}], 정상", new { });
}
else
{
if (await _logRepository.SaveLogUser(new LogUser
{
uid = user.uid, create_date = DateTime.Now, create_uid = "System", log = $"[{summary}] : 실패"
}))
{
_logger.LogInformation($"[{summary}]: 실패");
}
else
{
_logger.LogInformation($"[{summary}]: 실패 - 로그 저장 실패");
}
}
return APIResponse.InternalSeverError();
}
public async Task<APIResponseStatus<object>> GetAcademy(string summary, string token)
{
var validToken = await _jwtTokenService.ValidateToken(token);
if (validToken == null) return APIResponse.AccessExpireError();
var uid = validToken.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var user = await _userRepository.FindUser(uid);
if (user == null) return APIResponse.Send<object>("001", $"[{summary}], 회원 정보 확인 오류", new { });
var academyList = await _userRepository.FindAcademies(uid);
_logger.LogInformation($"[{summary}]: 성공 - {System.Text.Json.JsonSerializer.Serialize(academyList)}");
return APIResponse.Send<object>("000", $"[{summary}], 정상.", academyList);
}
}
}

View File

@ -5,3 +5,11 @@
- .NET Web API
### IDE
- JetBrains Rider
### 추가 패키지
| No. | Name | Version | Description |
|:---:|:---------------------------------------------:|:-------:|:---------------------------|
| 1 | Microsoft.AspNetCore.OpenApi | 8.0.10 | OpenAPI 를 지원하기 위해 사용 |
| 2 | Microsoft.EntityFrameworkCore | 8.0.10 | 데이터베이스 작업을 간편하게 수행하기 위해 사용 |
| 3 | Pomelo.EntityFrameworkCore.MySql | 8.0.2 | MariaDB 연결 |
| 4 | Microsoft.AspNetCore.Authentication.JwtBearer | 8.0.10 | |

92
SwaggerConfigure.cs Normal file
View File

@ -0,0 +1,92 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Back
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CustomOperationAttribute : Attribute
{
public string Summary { get; }
public string Description { get; }
public string[] Tags { get; }
public CustomOperationAttribute(string summary, string description, params string[] tags)
{
Summary = summary;
Description = description;
Tags = tags;
}
}
public class CustomSwaggerOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var customSwaggerAttribute = context.MethodInfo.GetCustomAttributes(typeof(CustomOperationAttribute), false)
.FirstOrDefault() as CustomOperationAttribute;
if (customSwaggerAttribute != null)
{
operation.Summary = customSwaggerAttribute.Summary;
operation.Description = customSwaggerAttribute.Description;
operation.Tags = customSwaggerAttribute.Tags
.Select(tag => new OpenApiTag { Name = tag })
.ToList();
}
}
}
public static class SwaggerConfigure
{
private static OpenApiInfo DocName(string title, string version)
{
return new OpenApiInfo
{
Title = title,
Version = version
};
}
public static void AddCustomSwagger(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
options.EnableAnnotations();
options.OperationFilter<CustomSwaggerOperationFilter>();
options.SwaggerDoc("전체", DocName("AcaMate 전체 API","1.0.0"));
options.SwaggerDoc("공통",DocName("공통 API", "1.0.0"));
options.SwaggerDoc("사업자 정보", DocName("사업자 정보 API", "1.0.0"));
options.SwaggerDoc("사용자", DocName("사용자 API", "1.0.0"));
options.DocInclusionPredicate((docName, apiDesc) =>
{
if (docName == "전체") return true; // 전체 문서에 모든 API 포함
if (docName == "공통" && apiDesc.GroupName == "공통") return true;
if (docName == "사업자 정보" && apiDesc.GroupName == "사업자 정보") return true;
if (docName == "사용자" && apiDesc.GroupName == "사용자") return true;
return false;
});
options.TagActionsBy(apiDesc => new[] { apiDesc.GroupName ?? "기타" });
// options.TagActionsBy(apiDesc => apiDesc.ActionDescriptor.EndpointMetadata
// .OfType<SwaggerOperationAttribute>()
// .FirstOrDefault()?.Tags ?? new[] { "기타" });
});
}
public static void UseCustomSwaggerUI(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/전체/swagger.json", "전체 API");
options.SwaggerEndpoint("/swagger/공통/swagger.json", "공통 API");
options.SwaggerEndpoint("/swagger/사용자/swagger.json", "사용자 API");
options.SwaggerEndpoint("/swagger/사업자 정보/swagger.json", "사업자 정보 API");
});
}
}
}

View File

@ -4,5 +4,19 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"PushFileSetting": {
"uri": "https://api.sandbox.push.apple.com/",
"p12Path": "./private/AM_Push_Sandbox.p12",
"p12PWPath": "./private/appleKeys.json",
"apnsTopic": "me.myds.ipstein.acamate.AcaMate"
},
"AllowedHosts": "*",
"Kakao": {
"ClientId": "a9632e6c14d8706ef6c8fe2ef52b721d",
"ClientSecret": " this is rest api secret key ",
// "RedirectUri": "http://0.0.0.0:5144/api/v1/out/user/kakao/redirect"
"RedirectUri": "https://devacamate.ipstein.myds.me/api/v1/out/user/kakao/redirect"
}
}

View File

@ -1,9 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"PushFileSetting": {
"Uri": "https://api.push.apple.com/",
"P12Path": "/src/private/AM_Push.p12",
"P12PWPath": "/src/private/appleKeys.json",
"ApnsTopic": "me.myds.ipstein.acamate.AcaMate"
},
"AllowedHosts": "*",
"Kakao": {
"ClientId": "a9632e6c14d8706ef6c8fe2ef52b721d",
"ClientSecret": " this is rest api secret key ",
"RedirectUri": "https://acamate.ipstein.myds.me/api/v1/out/user/kakao/redirect"
}
}

1669
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff