+
+ 체다 서비스에 로그인
+
@@ -15,7 +19,18 @@ export default function LoginPage() {
}
function LoginButton() {
+ const auth = useAuth();
+
+ const router = useRouter();
const searchParams = useSearchParams();
- return
;
+ const prevUrl = decodeURIComponent(searchParams.get('prevUrl') ?? '');
+
+ useEffect(() => {
+ if (auth.data?.loggedIn) {
+ router.push(prevUrl || '/');
+ }
+ }, [auth.data]);
+
+ return
;
}
diff --git a/apps/web/components/header/index.tsx b/apps/web/components/header/index.tsx
index 414b76b..68b754b 100644
--- a/apps/web/components/header/index.tsx
+++ b/apps/web/components/header/index.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import Image from 'next/image';
import Link from 'next/link';
@@ -6,6 +8,7 @@ import { Button } from "@/components/ui/button"
import ThemeToggle from "@/components/theme-toggle";
import { blackHanSans } from '@/app/fonts';
import { cn } from '@/lib/utils';
+import LoginButton from './login-button';
import logoImage from './cheda-transparent.png';
export default function Header() {
@@ -26,6 +29,9 @@ export default function Header() {
+
+
+
);
diff --git a/apps/web/components/header/login-button.tsx b/apps/web/components/header/login-button.tsx
new file mode 100644
index 0000000..7c640c3
--- /dev/null
+++ b/apps/web/components/header/login-button.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import Link from 'next/link';
+import { Skeleton } from '@/components/ui/skeleton';
+import { Button } from "@/components/ui/button";
+// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import useAuth from '@/hooks/useAuth';
+
+export default function LoginButton() {
+ const auth = useAuth();
+
+ if (auth.isLoading) {
+ return
;
+ }
+
+ if (!auth.data?.loggedIn) {
+ return (
+
+ );
+ }
+
+ // return (
+ //
+ //
+ // {auth.data.user.userName.substring(0, 1)}
+ //
+ // );
+
+ return (
+
+ );
+}
diff --git a/apps/web/components/ui/avatar.tsx b/apps/web/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/apps/web/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/apps/web/hooks/useAuth.ts b/apps/web/hooks/useAuth.ts
new file mode 100644
index 0000000..cbbf716
--- /dev/null
+++ b/apps/web/hooks/useAuth.ts
@@ -0,0 +1,91 @@
+import { useAtomValue } from 'jotai';
+import { atomWithQuery } from 'jotai-tanstack-query';
+import { importSPKI, jwtVerify } from 'jose';
+
+const JWT_PREFIX = 'http:cheda.kr/';
+
+type PrefixRoot = {
+ [k in keyof TValue as k extends string ? `${TPrefix}${k}` : never]: TValue[k];
+};
+
+type SessionPayload = PrefixRoot;
+
+type SecuredSessionPayload = PrefixRoot;
+
+type StatePayload = PrefixRoot;
+
+type Auth = {
+ loggedIn: false;
+ user: null;
+} | {
+ loggedIn: true;
+ user: {
+ userId: string;
+ userName: string;
+ userImage: string;
+ };
+};
+
+const authAtom = atomWithQuery(() => ({
+ queryKey: ['auth'],
+ queryFn: async () => {
+ const sessionId = document.cookie
+ .split('; ')
+ .find((cookie) => cookie.startsWith('session_id='))
+ ?.match(/^([^=]+)=(.*)/)
+ ?.[2];
+
+ try {
+ const publicKey = await importSPKI(process.env.NEXT_PUBLIC_JWT_KEY!, 'ES256');
+ const token = await jwtVerify(sessionId ?? '', publicKey);
+
+ return {
+ loggedIn: true,
+ user: token.payload['http:cheda.kr/user'],
+ };
+ } catch (e) {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_ORIGIN}/services/auth/v1/me`, {
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ return {
+ loggedIn: false,
+ user: null,
+ };
+ }
+
+ const result = await response.json();
+
+ return {
+ loggedIn: true,
+ user: {
+ userId: result.id,
+ userName: result.name,
+ userImage: result.image,
+ },
+ };
+ }
+ },
+}));
+
+export default function useAuth() {
+ return useAtomValue(authAtom);
+}
+
diff --git a/apps/web/package.json b/apps/web/package.json
index 54b4bc9..2dd6b8d 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,18 +12,24 @@
"dependencies": {
"@next/third-parties": "^14.2.1",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.0.2",
+ "@tanstack/query-core": "^5.36.0",
"@tanstack/react-query": "^5.29.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "jose": "^5.2.4",
+ "jotai": "^2.8.0",
+ "jotai-tanstack-query": "^0.8.5",
"lucide-react": "^0.368.0",
"next": "14.1.4",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.2.2",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "wonka": "^6.3.4"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9845b07..41999ab 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,12 +54,18 @@ importers:
'@radix-ui/react-accordion':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0)
+ '@radix-ui/react-avatar':
+ specifier: ^1.0.4
+ version: 1.0.4(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.0.0)(react@18.0.0)
+ '@tanstack/query-core':
+ specifier: ^5.36.0
+ version: 5.36.0
'@tanstack/react-query':
specifier: ^5.29.2
version: 5.29.2(react@18.0.0)
@@ -69,6 +75,15 @@ importers:
clsx:
specifier: ^2.1.0
version: 2.1.0
+ jose:
+ specifier: ^5.2.4
+ version: 5.2.4
+ jotai:
+ specifier: ^2.8.0
+ version: 2.8.0(@types/react@18.0.0)(react@18.0.0)
+ jotai-tanstack-query:
+ specifier: ^0.8.5
+ version: 0.8.5(@tanstack/query-core@5.36.0)(jotai@2.8.0)(wonka@6.3.4)
lucide-react:
specifier: ^0.368.0
version: 0.368.0(react@18.0.0)
@@ -90,6 +105,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.3.0)
+ wonka:
+ specifier: ^6.3.4
+ version: 6.3.4
devDependencies:
'@types/node':
specifier: ^20
@@ -1469,6 +1487,30 @@ packages:
react-dom: 18.0.0(react@18.0.0)
dev: false
+ /@radix-ui/react-avatar@1.0.4(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0):
+ resolution: {integrity: sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.24.1
+ '@radix-ui/react-context': 1.0.1(@types/react@18.0.0)(react@18.0.0)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0)
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.0.0)(react@18.0.0)
+ '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.0.0)(react@18.0.0)
+ '@types/react': 18.0.0
+ '@types/react-dom': 18.0.0
+ react: 18.0.0
+ react-dom: 18.0.0(react@18.0.0)
+ dev: false
+
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0)(react@18.0.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies:
@@ -2075,6 +2117,10 @@ packages:
resolution: {integrity: sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww==}
dev: false
+ /@tanstack/query-core@5.36.0:
+ resolution: {integrity: sha512-B5BD3pg/mztDR36i77hGcyySKKeYrbM5mnogOROTBi1SUml5ByRK7PGUUl16vvubvQC+mSnqziFG/VIy/DE3FQ==}
+ dev: false
+
/@tanstack/react-query@5.29.2(react@18.0.0):
resolution: {integrity: sha512-nyuWILR4u7H5moLGSiifLh8kIqQDLNOHGuSz0rcp+J75fNc8aQLyr5+I2JCHU3n+nJrTTW1ssgAD8HiKD7IFBQ==}
peerDependencies:
@@ -4312,6 +4358,34 @@ packages:
resolution: {integrity: sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==}
dev: false
+ /jotai-tanstack-query@0.8.5(@tanstack/query-core@5.36.0)(jotai@2.8.0)(wonka@6.3.4):
+ resolution: {integrity: sha512-cFq+1sE7Qkt7Kh9Db2KE8LXdbPiGwCiy8S5YSEnjUDxF59A4XhoXTDJBuPiMIA1dD1/yMsNKr1ADfN5CvscYZw==}
+ peerDependencies:
+ '@tanstack/query-core': '*'
+ jotai: '>=2.0.0'
+ wonka: ^6.3.4
+ dependencies:
+ '@tanstack/query-core': 5.36.0
+ jotai: 2.8.0(@types/react@18.0.0)(react@18.0.0)
+ wonka: 6.3.4
+ dev: false
+
+ /jotai@2.8.0(@types/react@18.0.0)(react@18.0.0):
+ resolution: {integrity: sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=17.0.0'
+ react: '>=17.0.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.0.0
+ react: 18.0.0
+ dev: false
+
/js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
dev: false
@@ -6143,6 +6217,10 @@ packages:
stackback: 0.0.2
dev: true
+ /wonka@6.3.4:
+ resolution: {integrity: sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg==}
+ dev: false
+
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true