Compare commits
23 Commits
645e0b033b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f319f2d772 | |||
| bc8dc79295 | |||
|
|
93c00298e3 | ||
|
|
32d29845cc | ||
|
|
5a91cca94f | ||
| fd8ea31fd8 | |||
|
|
1c1a4383e1 | ||
|
|
48f706cc42 | ||
| 497ac11f26 | |||
|
|
e6c532faae | ||
|
|
540d32a570 | ||
|
|
13094084d4 | ||
|
|
ba4bffc28e | ||
|
|
5622f439aa | ||
|
|
1cf6e54356 | ||
|
|
f9b4e8e7ca | ||
|
|
3552ad4c51 | ||
|
|
7e795083e5 | ||
|
|
cfa7f160d9 | ||
|
|
7eccb5f1c7 | ||
|
|
48744c4774 | ||
| 734015485b | |||
|
|
eaa0d7e820 |
194
package-lock.json
generated
194
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
88
src/app/components/ImgInputForm.module.css
Normal file
88
src/app/components/ImgInputForm.module.css
Normal 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;
|
||||
}
|
||||
160
src/app/components/ImgInputForm.tsx
Normal file
160
src/app/components/ImgInputForm.tsx
Normal 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
|
||||
@@ -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;
|
||||
@@ -3,6 +3,7 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
10
src/app/main/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
import ImgInputForm from '../components/ImgInputForm'
|
||||
|
||||
const MainPage = () => {
|
||||
|
||||
return <ImgInputForm />
|
||||
|
||||
}
|
||||
|
||||
export default MainPage
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
51
src/app/pages/oauth/callback.tsx
Normal file
51
src/app/pages/oauth/callback.tsx
Normal 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;
|
||||
BIN
src/assets/kakao_login_medium_narrow.png
Normal file
BIN
src/assets/kakao_login_medium_narrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
6
src/lib/Auth.ts
Normal file
6
src/lib/Auth.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const callServer = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
withCredentials: true
|
||||
})
|
||||
33
src/pages/api/oauth/kakao.ts
Normal file
33
src/pages/api/oauth/kakao.ts
Normal 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
12
src/utils/Login.ts
Normal 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
13
tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user