Skip to content

Commit b0d6f1a

Browse files
authored
Merge branch 'develop' into backend
2 parents 5832c81 + 7835130 commit b0d6f1a

File tree

179 files changed

+11815
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

179 files changed

+11815
-188
lines changed

.github/labeler.yml

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,4 @@ labels:
2727

2828
':hammer: REFACTOR':
2929
include:
30-
['\bRefactor\b', '\bREFACTOR\b', '\brefactor\b']
31-
32-
':nail_care: FRONTEND':
33-
include:
34-
['\bFrontend\b', '\bfrontend\b', '\bFRONTEND\b']
35-
36-
':page_facing_up: AI':
37-
include:
38-
['\bAI\b', '\bai\b', '\bAi\b']
39-
40-
':rocket: BACKEND':
41-
include:
42-
['\bbackend\b', '\bBackend\b', '\bBACKEND\b']
30+
['\bRefactor\b', '\bREFACTOR\b', '\brefactor\b']

.github/workflows/backend-cd.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Secure Backend CD
2+
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
paths:
8+
- "backend/**"
9+
10+
jobs:
11+
deploy:
12+
runs-on: ubuntu-latest
13+
environment: secretconfig
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
18+
- name: Set up JDK 21
19+
uses: actions/setup-java@v4
20+
with:
21+
java-version: '21'
22+
distribution: 'temurin'
23+
24+
- name: Grant execute permission for gradlew
25+
run: chmod +x ./backend/gradlew
26+
27+
- name: Build backend application
28+
working-directory: ./backend
29+
run: ./gradlew build -x test
30+
31+
- name: Prepare SSH key
32+
run: |
33+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > key.pem
34+
chmod 600 key.pem
35+
36+
- name: Add remote host to known_hosts
37+
run: |
38+
mkdir -p ~/.ssh
39+
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
40+
41+
- name: Copy JAR to EC2 server
42+
run: |
43+
scp -i key.pem ./backend/build/libs/backend-0.0.1-SNAPSHOT.jar ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/capstone-2025-24/backend/build/libs/
44+
45+
- name: Deploy on EC2 via SSH
46+
env:
47+
APP_YML: ${{ secrets.APPLICATION_YML }}
48+
DOCKER_COMPOSE_YML: ${{ secrets.DOCKER_COMPOSE_YML }}
49+
run: |
50+
ssh -i key.pem ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
51+
cd ~/capstone-2025-24
52+
git pull
53+
cat > backend/src/main/resources/application.yml << 'END_YML'
54+
$APP_YML
55+
END_YML
56+
cat > docker-compose.yml << 'END_DOCKER'
57+
$DOCKER_COMPOSE_YML
58+
END_DOCKER
59+
docker-compose down
60+
docker-compose build
61+
docker-compose up -d
62+
EOF

.github/workflows/frontend-ci.yml

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,20 @@ jobs:
1919
- name: Checkout code
2020
uses: actions/checkout@v3
2121

22+
- name: Check file structure
23+
run: ls -R
24+
2225
- name: Set up Node.js
2326
uses: actions/setup-node@v3
2427
with:
2528
node-version: ${{ matrix.node-version }}
26-
cache: 'yarn'
29+
cache: "yarn"
30+
cache-dependency-path: "./frontend/yarn.lock"
2731

2832
- name: Install dependencies
29-
run: |
30-
cd frontend
31-
yarn install --frozen-lockfile
32-
33-
- name: Run tests
34-
run: |
35-
cd frontend
36-
yarn test
33+
working-directory: frontend
34+
run: yarn install --frozen-lockfile
3735

3836
- name: Build project
39-
run: |
40-
cd frontend
41-
yarn build
37+
working-directory: frontend
38+
run: yarn build

.github/workflows/label.yml

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
types: [opened]
66
pull_request_target:
77
types: [opened]
8-
8+
99
jobs:
1010
labeler:
1111
runs-on: ubuntu-latest
@@ -18,4 +18,35 @@ jobs:
1818
id: labeler
1919
uses: jimschubert/labeler-action@v1
2020
with:
21-
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
21+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
22+
23+
- name: Add labels based on user
24+
uses: actions/github-script@v7
25+
with:
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
script: |
28+
const author = context.payload.pull_request
29+
? context.payload.pull_request.user.login
30+
: context.payload.issue.user.login;
31+
32+
const userLabels = {
33+
"hummingbbird": ":nail_care: FRONTEND",
34+
"HeeNamgoong": ":nail_care: FRONTEND",
35+
"seo0o519": ":nail_care: FRONTEND",
36+
"hyni03": ":rocket: BACKEND",
37+
"mjk25": ":rocket: BACKEND",
38+
"justpers": ":page_facing_up: AI"
39+
};
40+
41+
const labelToAdd = userLabels[author];
42+
43+
if (labelToAdd) {
44+
github.rest.issues.addLabels({
45+
owner: context.repo.owner,
46+
repo: context.repo.repo,
47+
issue_number: context.payload.pull_request
48+
? context.payload.pull_request.number
49+
: context.payload.issue.number,
50+
labels: [labelToAdd]
51+
});
52+
}

backend/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM openjdk:21
2+
WORKDIR /app
3+
COPY build/libs/*.jar app.jar
4+
ENTRYPOINT ["java", "-jar", "app.jar"]

backend/src/main/java/com/example/backend/analysis/controller/CaseStatsController.java

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.springframework.http.ResponseEntity;
88
import org.springframework.web.bind.annotation.*;
99

10+
import java.util.Collections;
1011
import java.util.List;
1112
import java.util.Map;
13+
import java.util.NoSuchElementException;
1214

1315
@RestController
1416
@RequestMapping("/api/v1/stats")
@@ -20,18 +22,33 @@ public class CaseStatsController {
2022
// 개요 조회
2123
@GetMapping("/overview")
2224
public ResponseEntity<?> getOverview(HttpSession session) {
23-
// 결과가 없으면 404, 있으면 200
24-
CaseStatsOverviewResponse response = caseStatsService.getOverview(session);
25-
return ResponseEntity.ok(response);
25+
try {
26+
CaseStatsOverviewResponse response = caseStatsService.getOverview(session);
27+
return ResponseEntity.ok(response);
28+
} catch (IllegalStateException e) {
29+
return ResponseEntity.status(401).body(Collections.singletonMap("message", e.getMessage()));
30+
} catch (NoSuchElementException e) {
31+
return ResponseEntity.status(404).body(Collections.singletonMap("message", e.getMessage()));
32+
} catch (Exception e) {
33+
return ResponseEntity.status(500).body(Collections.singletonMap("message", "내부 서버 오류 발생"));
34+
}
2635
}
2736

2837
// 시간대별 사건 수 조회 (0시~23시 모두 반환)
2938
@GetMapping("/hour")
3039
public ResponseEntity<?> getHourlyCaseStats(@RequestParam("date") String date,
3140
@RequestParam(value = "category", required = false) String category,
3241
HttpSession session) {
33-
List<HourlyCaseStatsResponse> stats = caseStatsService.getHourlyCaseStats(date, category, session);
34-
return ResponseEntity.ok(stats);
42+
try {
43+
List<HourlyCaseStatsResponse> stats = caseStatsService.getHourlyCaseStats(date, category, session);
44+
return ResponseEntity.ok(stats);
45+
} catch (IllegalStateException e) {
46+
return ResponseEntity.status(401).body(Collections.singletonMap("message", e.getMessage()));
47+
} catch (NoSuchElementException e) {
48+
return ResponseEntity.status(404).body(Collections.singletonMap("message", e.getMessage()));
49+
} catch (Exception e) {
50+
return ResponseEntity.status(500).body(Collections.singletonMap("message", "내부 서버 오류가 발생했습니다."));
51+
}
3552
}
3653

3754
// 월별/일별 사건 수 조회 (month 파라미터 존재 여부에 따라 분기; 전체 범위를 0으로 채워 반환)
@@ -40,29 +57,54 @@ public ResponseEntity<?> getCaseStats(@RequestParam("year") int year,
4057
@RequestParam(value = "month", required = false) Integer month,
4158
@RequestParam(value = "category", required = false) String category,
4259
HttpSession session) {
43-
// month가 없으면 월별 데이터
44-
if (month == null) {
45-
List<MonthlyCaseStatsResponse> monthlyStats = caseStatsService.getMonthlyCaseStats(year, category, session);
46-
return ResponseEntity.ok(monthlyStats);
60+
try {
61+
if (month == null) {
62+
List<MonthlyCaseStatsResponse> monthlyStats = caseStatsService.getMonthlyCaseStats(year, category, session);
63+
return ResponseEntity.ok(monthlyStats);
64+
}
65+
List<DailyCaseStatsResponse> dailyStats = caseStatsService.getDailyCaseStats(year, month, category, session);
66+
return ResponseEntity.ok(dailyStats);
67+
} catch (IllegalStateException e) {
68+
return ResponseEntity.status(401).body(Collections.singletonMap("message", e.getMessage()));
69+
} catch (NoSuchElementException e) {
70+
return ResponseEntity.status(404).body(Collections.singletonMap("message", e.getMessage()));
71+
} catch (Exception e) {
72+
return ResponseEntity.status(500).body(Collections.singletonMap("message", "내부 서버 오류가 발생했습니다."));
4773
}
48-
49-
// month가 있으면 일별 데이터
50-
List<DailyCaseStatsResponse> dailyStats = caseStatsService.getDailyCaseStats(year, month, category, session);
51-
return ResponseEntity.ok(dailyStats);
5274
}
5375

5476
// 유형별 사건 수 조회 (기본 카테고리 0 포함)
5577
@GetMapping("/category")
5678
public ResponseEntity<?> getCategoryCaseStats(@RequestParam("period") String period, HttpSession session) {
57-
Map<String, Integer> stats = caseStatsService.getCategoryCaseStats(period, session);
58-
return ResponseEntity.ok(stats);
79+
try {
80+
Map<String, Integer> stats = caseStatsService.getCategoryCaseStats(period, session);
81+
return ResponseEntity.ok(stats);
82+
} catch (IllegalStateException e) {
83+
return ResponseEntity.status(401).body(Collections.singletonMap("message", e.getMessage()));
84+
} catch (IllegalArgumentException e) {
85+
return ResponseEntity.status(400).body(Collections.singletonMap("message", e.getMessage()));
86+
} catch (NoSuchElementException e) {
87+
return ResponseEntity.status(404).body(Collections.singletonMap("message", e.getMessage()));
88+
} catch (Exception e) {
89+
return ResponseEntity.status(500).body(Collections.singletonMap("message", "내부 서버 오류가 발생했습니다."));
90+
}
5991
}
6092

6193
// 장소별 사건 수 조회
6294
@GetMapping("/location")
6395
public ResponseEntity<?> getLocationCaseStats(@RequestParam("period") String period, HttpSession session) {
64-
List<LocationCaseStatsResponse> stats = caseStatsService.getLocationCaseStats(period, session);
65-
return ResponseEntity.ok(stats);
96+
try {
97+
List<LocationCaseStatsResponse> stats = caseStatsService.getLocationCaseStats(period, session);
98+
return ResponseEntity.ok(stats);
99+
} catch (IllegalStateException e) {
100+
return ResponseEntity.status(401).body(Collections.singletonMap("message", e.getMessage()));
101+
} catch (IllegalArgumentException e) {
102+
return ResponseEntity.status(400).body(Collections.singletonMap("message", e.getMessage()));
103+
} catch (NoSuchElementException e) {
104+
return ResponseEntity.status(404).body(Collections.singletonMap("message", e.getMessage()));
105+
} catch (Exception e) {
106+
return ResponseEntity.status(500).body(Collections.singletonMap("message", "내부 서버 오류가 발생했습니다."));
107+
}
66108
}
67109

68110
// 지도용 장소별 사건 수 조회
@@ -71,5 +113,4 @@ public ResponseEntity<?> getMapCaseStats(HttpSession session) {
71113
List<MapCaseStatsResponse> stats = caseStatsService.getMapCaseStats(session);
72114
return ResponseEntity.ok(stats);
73115
}
74-
75116
}
Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
package com.example.backend.analysis.dto;
22

3-
import com.example.backend.common.domain.CaseEntity;
4-
import com.example.backend.common.domain.CaseStatsOverviewEntity;
53
import lombok.*;
64

75
@Data
8-
@Builder
9-
@NoArgsConstructor
106
@AllArgsConstructor
117
public class CaseStatsOverviewResponse {
128
private int recentCase;
139
private int todayCase;
14-
private CaseEntity.CaseCategory mostCase;
10+
private String mostCase;
1511
private String patrolRegion;
16-
17-
public static CaseStatsOverviewResponse fromEntity(CaseStatsOverviewEntity entity, String patrolRegionAddress) {
18-
return CaseStatsOverviewResponse.builder()
19-
.recentCase(entity.getRecentCaseCount())
20-
.todayCase(entity.getTodayCaseCount())
21-
.mostCase(entity.getMostCase())
22-
.patrolRegion(patrolRegionAddress)
23-
.build();
24-
}
2512
}

backend/src/main/java/com/example/backend/analysis/dto/DailyCaseStatsResponse.java

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,15 @@
22

33
import lombok.*;
44

5-
@Data
6-
@Builder
7-
@NoArgsConstructor
5+
@Getter
6+
@Setter
87
@AllArgsConstructor
8+
@NoArgsConstructor
99
public class DailyCaseStatsResponse {
1010
private int day;
1111
private int fireCount;
1212
private int assaultCount;
1313
private int crowdCongestionCount;
1414
private int weaponCount;
1515
private int swoonCount;
16-
17-
public static DailyCaseStatsResponse fromRow(Object[] row) {
18-
return DailyCaseStatsResponse.builder()
19-
.day(((Number) row[0]).intValue())
20-
.fireCount(((Number) row[1]).intValue())
21-
.assaultCount(((Number) row[2]).intValue())
22-
.crowdCongestionCount(((Number) row[3]).intValue())
23-
.weaponCount(((Number) row[4]).intValue())
24-
.swoonCount(((Number) row[5]).intValue())
25-
.build();
26-
}
2716
}
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package com.example.backend.analysis.dto;
22

3-
import lombok.*;
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
45

56
@Data
6-
@Builder
7-
@NoArgsConstructor
87
@AllArgsConstructor
98
public class HourlyCaseStatsResponse {
109
private int hour;
@@ -13,15 +12,4 @@ public class HourlyCaseStatsResponse {
1312
private int crowdCongestionCount;
1413
private int weaponCount;
1514
private int swoonCount;
16-
17-
public static HourlyCaseStatsResponse fromRow(Object[] row) {
18-
return HourlyCaseStatsResponse.builder()
19-
.hour(((Number) row[0]).intValue())
20-
.fireCount(((Number) row[1]).intValue())
21-
.assaultCount(((Number) row[2]).intValue())
22-
.crowdCongestionCount(((Number) row[3]).intValue())
23-
.weaponCount(((Number) row[4]).intValue())
24-
.swoonCount(((Number) row[5]).intValue())
25-
.build();
26-
}
2715
}

0 commit comments

Comments
 (0)