Skip to content

Commit dc54c89

Browse files
authored
Merge pull request #50 from Turtle-Hwan/feat/api
Feat/api | internal apis 연동 / 홈페이지 공지사항 연동
2 parents c3abe0f + a32a93d commit dc54c89

28 files changed

+1966
-71
lines changed

.env.development.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ VITE_ENVIRONMENT=development
2727
# Google Analytics Measurement Protocol API Secret
2828
# 아래 값을 실제 API Secret으로 교체하세요
2929
VITE_GA_API_SECRET=your_api_secret_here
30+
31+
# 우리 Backend 주소
32+
VITE_API_BASE_URL=https://this-is-linku-backend.example/api

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ gh-pages
2929
*.sln
3030
*.sw?
3131

32-
.claude
32+
.claude
33+
docs

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "linku",
33
"private": true,
4-
"version": "1.4.0",
4+
"version": "1.5.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite --mode development",
@@ -27,6 +27,7 @@
2727
"lucide-react": "^0.475.0",
2828
"react": "^19.1.0",
2929
"react-dom": "^19.1.0",
30+
"react-error-boundary": "^6.0.0",
3031
"sonner": "^2.0.7",
3132
"tailwind-merge": "^3.2.0",
3233
"tailwindcss-animate": "^1.0.7"

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "LinKU",
44
"description": "건국대학교 학생들을 위한 건국대, 건대 교내외 페이지 모음 링쿠, LinKU",
5-
"version": "1.5.4",
5+
"version": "1.5.6",
66
"action": {
77
"default_popup": "index.html"
88
},

src/App.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect } from "react";
22
import "./App.css";
3+
import { ErrorBoundary } from "react-error-boundary";
34
import MainLayout from "./components/MainLayout";
45
import TabsLayout from "./components/TabsLayout";
56
import { Toaster } from "./components/ui/sonner";
@@ -18,12 +19,28 @@ function App() {
1819
}, []);
1920

2021
return (
21-
<>
22+
<ErrorBoundary
23+
fallback={
24+
<div className="w-[500px] h-[600px] flex items-center justify-center p-8">
25+
<div className="text-center space-y-4">
26+
<h2 className="text-xl font-semibold text-destructive">
27+
오류가 발생했습니다
28+
</h2>
29+
<button
30+
onClick={() => window.location.reload()}
31+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
32+
>
33+
새로고침
34+
</button>
35+
</div>
36+
</div>
37+
}
38+
>
2239
<MainLayout>
2340
<TabsLayout />
2441
</MainLayout>
2542
<Toaster duration={2000} />
26-
</>
43+
</ErrorBoundary>
2744
);
2845
}
2946

src/apis/alerts.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Alerts API
3+
* Notification and subscription management
4+
*/
5+
6+
import { get, post, del, ENDPOINTS } from './client';
7+
import type {
8+
ApiResponse,
9+
GeneralAlert,
10+
AlertFilterParams,
11+
Department,
12+
Subscription,
13+
} from '../types/api';
14+
import { getAlertsFromRSS } from './external/rss-parser';
15+
import { getCareerAlertsFromHTML } from './external/html-parser';
16+
17+
/**
18+
* Get filtered alerts by category
19+
* Fetch alerts with optional category filter
20+
* Falls back to external sources (RSS/HTML) for categories that fail via API
21+
*/
22+
export async function getAlerts(
23+
params?: AlertFilterParams
24+
): Promise<ApiResponse<GeneralAlert[]>> {
25+
try {
26+
// If specific category is requested, try API first
27+
if (params?.category) {
28+
const result = await get<GeneralAlert[]>(ENDPOINTS.ALERTS.BASE, { params });
29+
30+
// If API succeeded, return the result
31+
if (result.success && result.status === 200) {
32+
return result;
33+
}
34+
35+
// If API failed for this category, fallback to external sources
36+
console.warn(`API failed for category ${params.category}, falling back to external sources`);
37+
38+
const [rssAlerts, careerAlerts] = await Promise.all([
39+
getAlertsFromRSS(),
40+
getCareerAlertsFromHTML(6000),
41+
]);
42+
43+
const allAlerts = [...rssAlerts, ...careerAlerts];
44+
const filteredAlerts = allAlerts.filter(alert => alert.category === params.category);
45+
46+
return {
47+
success: true,
48+
data: filteredAlerts,
49+
status: 200,
50+
};
51+
}
52+
53+
// If no category specified, try API first for all categories
54+
const result = await get<GeneralAlert[]>(ENDPOINTS.ALERTS.BASE);
55+
56+
// If API succeeded, return the result
57+
if (result.success && result.status === 200) {
58+
return result;
59+
}
60+
61+
// If API failed, fallback to external sources for all categories
62+
console.warn("API failed, falling back to external sources");
63+
64+
const [rssAlerts, careerAlerts] = await Promise.all([
65+
getAlertsFromRSS(),
66+
getCareerAlertsFromHTML(6000),
67+
]);
68+
69+
const allAlerts = [...rssAlerts, ...careerAlerts];
70+
71+
return {
72+
success: true,
73+
data: allAlerts,
74+
status: 200,
75+
};
76+
} catch (error) {
77+
// If API and external sources fail, return error
78+
console.error("API and external sources failed:", error);
79+
return {
80+
success: false,
81+
error: {
82+
code: "FETCH_FAILED",
83+
message: "공지사항을 불러오는데 실패했습니다.",
84+
},
85+
};
86+
}
87+
}
88+
89+
/**
90+
* Get my alerts
91+
* Fetch alerts from subscribed departments
92+
*/
93+
export async function getMyAlerts(): Promise<ApiResponse<GeneralAlert[]>> {
94+
return get<GeneralAlert[]>(ENDPOINTS.ALERTS.MY);
95+
}
96+
97+
/**
98+
* Get all available departments for subscription
99+
*/
100+
export async function getSubscriptions(): Promise<ApiResponse<Department[]>> {
101+
return get<Department[]>(ENDPOINTS.ALERTS.SUBSCRIPTION);
102+
}
103+
104+
/**
105+
* Get my subscribed departments
106+
*/
107+
export async function getMySubscriptions(): Promise<ApiResponse<Subscription[]>> {
108+
return get<Subscription[]>(ENDPOINTS.ALERTS.MY_SUBSCRIPTION);
109+
}
110+
111+
/**
112+
* Subscribe to a department
113+
* Start receiving alerts from the department
114+
*/
115+
export async function subscribeDepartment(
116+
departmentId: number
117+
): Promise<ApiResponse<Subscription>> {
118+
return post<Subscription>(ENDPOINTS.ALERTS.SUBSCRIBE(departmentId));
119+
}
120+
121+
/**
122+
* Unsubscribe from a department
123+
* Stop receiving alerts from the department
124+
*/
125+
export async function unsubscribeDepartment(
126+
departmentId: number
127+
): Promise<ApiResponse<{ message: string }>> {
128+
return del<{ message: string }>(ENDPOINTS.ALERTS.UNSUBSCRIBE(departmentId));
129+
}

src/apis/auth.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Auth API
3+
* Authentication and verification operations
4+
*/
5+
6+
import { post, ENDPOINTS } from './client';
7+
import type {
8+
ApiResponse,
9+
GoogleOAuthRequest,
10+
GoogleOAuthResponse,
11+
SendCodeRequest,
12+
SendCodeResponse,
13+
VerifyCodeRequest,
14+
VerifyCodeResponse,
15+
} from '../types/api';
16+
17+
/**
18+
* Google OAuth authorization
19+
* Login or get guest token through Google OAuth2
20+
*/
21+
export async function googleOAuth(
22+
data: GoogleOAuthRequest
23+
): Promise<ApiResponse<GoogleOAuthResponse>> {
24+
return post<GoogleOAuthResponse>(ENDPOINTS.AUTH.GOOGLE_OAUTH, data);
25+
}
26+
27+
/**
28+
* Send verification code to email
29+
* Send 6-digit verification code to Konkuk University email
30+
*/
31+
export async function sendVerificationCode(
32+
data: SendCodeRequest
33+
): Promise<ApiResponse<SendCodeResponse>> {
34+
return post<SendCodeResponse>(ENDPOINTS.AUTH.SEND_CODE, data);
35+
}
36+
37+
/**
38+
* Verify email code
39+
* Verify 6-digit code sent to email
40+
*/
41+
export async function verifyEmailCode(
42+
data: VerifyCodeRequest
43+
): Promise<ApiResponse<VerifyCodeResponse>> {
44+
return post<VerifyCodeResponse>(ENDPOINTS.AUTH.VERIFY_CODE, data);
45+
}

0 commit comments

Comments
 (0)