Compare commits

...

23 Commits

Author SHA1 Message Date
f319f2d772 Merge branch 'main' of https://git.byeori.cloud/admin/ocr-nextjs
merge
2026-03-25 23:17:14 +09:00
bc8dc79295 이미지 인풋박스 설정 2026-03-25 22:08:55 +09:00
root
93c00298e3 add 2026-02-27 18:15:36 +09:00
root
32d29845cc kakaots 추가 2026-02-27 18:14:21 +09:00
root
5a91cca94f 로그인 프론트 구조 변경 2026-02-27 18:13:28 +09:00
fd8ea31fd8 프론트 수정 2026-02-02 23:14:53 +09:00
root
1c1a4383e1 서버에 이미지 전송 2026-01-28 19:14:32 +09:00
root
48f706cc42 파일 업로드 수정, css 고치기 2026-01-25 17:10:02 +09:00
497ac11f26 로그인여부 확인하는 요청 2025-12-29 18:03:43 +09:00
root
e6c532faae add tailwind 2025-12-26 20:51:37 +09:00
root
540d32a570 add 2025-12-26 20:18:08 +09:00
root
13094084d4 add main 2025-12-26 20:17:17 +09:00
root
ba4bffc28e 링크는 내부여서 외부인 a로 바꿈 2025-12-17 16:59:50 +09:00
root
5622f439aa 로그인 2025-09-08 12:40:30 +09:00
root
1cf6e54356 change redirect uri 2025-08-27 13:01:32 +09:00
root
f9b4e8e7ca complete 2025-08-04 12:58:31 +09:00
root
3552ad4c51 로그인 성공 정리필요 2025-07-25 16:25:07 +09:00
root
7e795083e5 login 2025-07-23 18:45:58 +09:00
root
cfa7f160d9 수정 2025-07-22 18:47:03 +09:00
root
7eccb5f1c7 로그인 버튼 추가 2025-07-22 18:34:42 +09:00
hanwha
48744c4774 change loginform port 2025-07-21 11:53:27 +09:00
734015485b login page 2025-07-16 23:37:14 +09:00
hanwha
eaa0d7e820 modify 2025-07-16 12:54:37 +09:00
17 changed files with 590 additions and 128 deletions

194
package-lock.json generated
View File

@@ -19,7 +19,9 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
},
@@ -671,6 +673,13 @@
"tailwindcss": "4.1.11"
}
},
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz",
@@ -917,6 +926,13 @@
"tailwindcss": "4.1.11"
}
},
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.7.tgz",
@@ -949,6 +965,43 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001760",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
@@ -959,6 +1012,50 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -983,9 +1080,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"version": "1.0.30001761",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"funding": [
{
"type": "opencollective",
@@ -999,7 +1096,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chownr": {
"version": "3.0.0",
@@ -1112,6 +1210,13 @@
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@@ -1166,6 +1271,16 @@
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@@ -1200,6 +1315,20 @@
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1768,6 +1897,13 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
@@ -1831,6 +1967,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -1840,6 +1977,13 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.26.9",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz",
@@ -1999,10 +2143,11 @@
}
},
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"dev": true
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.2",
@@ -2054,6 +2199,37 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",

View File

@@ -20,7 +20,9 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5"
}
}

View File

View File

@@ -0,0 +1,88 @@
.wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-height: 100vh;
width: 100%;
padding: 1rem;
}
.fileInput {
display: none;
}
.row {
width: 100%;
max-width: 520px;
display: flex;
gap: 0.5rem;
align-items: center;
}
.textInput {
flex: 1;
height: 2.75rem;
padding: 0.65rem 0.75rem;
border: 1px solid #cbd5e1;
border-radius: 0.55rem;
background-color: white;
color: #334155;
font-size: 0.95rem;
outline: none;
}
.textInput:focus {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
border-color: #2563eb;
}
.btn {
height: 2.75rem;
padding: 0.6rem 1rem;
border-radius: 0.55rem;
background-color: #6366F1;
color: #ffffff;
font-weight: 700;
cursor: pointer;
}
.btn:hover {
background-color: #4F46E5;
}
.btnLoading {
background-color: #A5B4FC;
cursor: wait;
}
.messageArea {
width: 100%;
max-width: 520px;
margin-top: 0.75rem;
text-align: left;
}
.errorMsg {
margin: 0.25rem 0 0;
color: #dc2626;
}
.successMsg {
margin: 0.25rem 0 0;
color: #16a34a;
}
.previewContainer {
margin-bottom: 1.5rem;
display: flex;
justify-content: center;
}
.previewImage {
width: 200px;
height: 120px;
border-radius: 0.55rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
object-fit: cover;
}

View File

@@ -0,0 +1,160 @@
"use client"
import { useCallback, useRef, useState, useEffect } from 'react'
import styles from './ImgInputForm.module.css'
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
const ImgInputForm = () => {
const fileRef = useRef<HTMLInputElement | null>(null)
const [fileName, setFileName] = useState('')
const [preview, setPreview] = useState<string | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
//process.env.NEXT_PUBLIC_API_URL ||
const API_BASE = 'http://localhost:9001'
useEffect(() => {
const checkAuth = async () => {
try {
const res = await fetch(`${API_BASE}/auth/check-token`, {
method: 'GET',
credentials: 'include',
})
if (!res.ok) throw new Error('인증 실패')
console.log('사용자 인증 성공')
} catch (err) {
console.error('사용자 인증 실패:', (err as Error).message)
setError('로그인이 필요합니다.')
}
}
checkAuth()
}, [])
const handleFile = useCallback((file: File) => {
setError(null)
setSuccess(null)
if (!file.type.startsWith('image/')) {
setError('이미지 파일만 업로드할 수 있습니다.')
return
}
if (file.size > MAX_FILE_SIZE) {
setError('파일 크기는 5MB 이하만 업로드 가능합니다.')
return
}
setFileName(file.name)
setPreview(URL.createObjectURL(file))
setShowPreview(false)
// keep the selected file in the hidden input
if (fileRef.current) {
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
fileRef.current.files = dataTransfer.files
}
}, [])
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.[0]
if (file) handleFile(file)
}
const onClickPick = () => fileRef.current?.click()
const onDrop: React.DragEventHandler<HTMLDivElement> = (e) => {
e.preventDefault()
setIsDragging(false)
const file = e.dataTransfer.files?.[0]
if (file) handleFile(file)
}
const onDragOver: React.DragEventHandler<HTMLDivElement> = (e) => {
e.preventDefault()
setIsDragging(true)
}
const onDragLeave: React.DragEventHandler<HTMLDivElement> = () => {
setIsDragging(false)
}
const upload = async () => {
setError(null)
setSuccess(null)
const files = fileRef.current?.files
if (!files || files.length === 0) {
setError('업로드할 파일을 선택해주세요.')
return
}
const file = files[0]
if (!file.type.startsWith('image/')) {
setError('이미지 파일만 업로드할 수 있습니다.')
return
}
setLoading(true)
setShowPreview(false)
try {
const fd = new FormData()
fd.append('image', file)
const res = await fetch(`${API_BASE}/img/get-img`, {
method: 'POST',
body: fd,
credentials: 'include'
})
if (!res.ok) throw new Error('업로드 실패')
setSuccess('업로드 성공')
setShowPreview(true)
} catch (err) {
setError((err as Error).message || '업로드 중 오류가 발생했습니다')
} finally {
setLoading(false)
}
}
return (
<>
<div className={styles.wrapper}>
<input
ref={fileRef}
type="file"
accept="image/*"
className={styles.fileInput}
onChange={onChange}
/>
{preview && (
<div className={styles.previewContainer}>
<img src={preview} alt="selected preview" className={styles.previewImage} />
</div>
)}
<div className={styles.row}>
<input
type="text"
readOnly
value={fileName}
onClick={onClickPick}
placeholder="선택된 파일 없음"
className={styles.textInput}
/>
<button
onClick={upload}
disabled={loading || !fileName}
className={`${styles.btn} ${loading ? styles.btnLoading : styles.btnPrimary}`}
>
{loading ? '업로드 중...' : '업로드'}
</button>
</div>
{error && <p className={`${styles.errorMsg}`}>{error}</p>}
{success && <p className={styles.successMsg}>{success}</p>}
</div>
</>
)
}
export default ImgInputForm

View File

@@ -1,33 +1,22 @@
'use client';
import axios from "axios";
import { useState } from "react";
import loginButton from '@/assets/kakao_login_medium_narrow.png';
import Link from "next/link";
const LoginForm = () => {
const [id, setId] = useState('') //ID
const [pw, setPw] = useState('') //PW
const [param, setParam] = useState()
const login = () => {
axios.post('login/oauth-kakao', {
})
}
const requestUrl = 'https://kauth.kakao.com/oauth/authorize?client_id=a1d6afef2d4508a10a498b7069f67496&redirect_uri=http://localhost:9001/oauth/oauth-kakao-authorize&response_type=code'
return (
<>
<input type="text"
placeholder="ID"
value={id}
onChange={(e) => {setId(e.target.value)}}/>
<input type="password"
placeholder="PW"
value={pw}
onChange={(e) => {setPw(e.target.value)}}/>
<button type="button" onClick={login}></button>
<a href={requestUrl}>
<button type="button">
<img src={loginButton.src} alt="카카오톡 로그인" />
</button>
</a>
</>
)
}
export default LoginForm;

View File

@@ -3,6 +3,7 @@
:root {
--background: #ffffff;
--foreground: #171717;
height: 100%;
}
@theme inline {

View File

@@ -23,10 +23,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<html lang="ko" className="h-full">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased h-full min-h-screen`} >
{children}
</body>
</html>

View File

@@ -1,2 +1,14 @@
'use client';
import LoginForm from '../components/LoginForm';
const LoginPage = () => {
return (
<div>
<h1></h1>
<LoginForm />
</div>
);
};
export default LoginPage;

10
src/app/main/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import ImgInputForm from '../components/ImgInputForm'
const MainPage = () => {
return <ImgInputForm />
}
export default MainPage

View File

@@ -1,103 +1,14 @@
import Image from "next/image";
import LoginForm from "./components/LoginForm";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<LoginForm />
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View File

@@ -0,0 +1,51 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
const OAuthCallback = () => {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
useEffect(() => {
const handleOAuthCallback = async () => {
console.log('OAuth 콜백 처리 중...');
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
setError('카카오 로그인 중 오류가 발생했습니다.');
return;
}
if (code) {
try {
const res = await fetch('/api/oauth/kakao', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error('서버 요청 실패');
const data = await res.json();
setMessage(`로그인 성공: ${data.message}`);
} catch (err) {
setError((err as Error).message || '로그인 처리 중 오류가 발생했습니다.');
}
}
};
handleOAuthCallback();
}, []);
return (
<div>
{error && <p style={{ color: 'red' }}>{error}</p>}
{message && <p>{message}</p>}
</div>
);
};
export default OAuthCallback;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

6
src/lib/Auth.ts Normal file
View File

@@ -0,0 +1,6 @@
import axios from "axios";
export const callServer = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true
})

View File

@@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method Not Allowed' });
}
const { code } = req.body;
if (!code) {
return res.status(400).json({ message: 'Authorization code is missing' });
}
try {
const tokenResponse = await fetch('https://kauth.kakao.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.KAKAO_CLIENT_ID!,
redirect_uri: process.env.KAKAO_REDIRECT_URI!,
code,
}),
});
if (!tokenResponse.ok) throw new Error('Failed to fetch access token');
const tokenData = await tokenResponse.json();
res.status(200).json({ message: '카카오 로그인 성공', token: tokenData });
} catch (err) {
res.status(500).json({ message: (err as Error).message });
}
}

12
src/utils/Login.ts Normal file
View File

@@ -0,0 +1,12 @@
import { callServer } from "@/lib/Auth";
import { redirect } from "next/navigation";
export async function isLogin() {
const res = callServer.get('/login/get-user-info').then(res => {
const userInfo = res.data
console.log(userInfo)
if (!userInfo) {
redirect('/login')
}
})
}

13
tailwind.config.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}