feat: Inquiry sample page 구현
- 문의 목록 페이지 - 문의 작성 페이지
This commit is contained in:
parent
65a30310a5
commit
a684a3e5be
@ -14,6 +14,7 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.71.0",
|
"@tanstack/react-query-devtools": "^5.71.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"iron-session": "^8.0.4",
|
"iron-session": "^8.0.4",
|
||||||
|
"lucide": "^0.503.0",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ importers:
|
|||||||
iron-session:
|
iron-session:
|
||||||
specifier: ^8.0.4
|
specifier: ^8.0.4
|
||||||
version: 8.0.4
|
version: 8.0.4
|
||||||
|
lucide:
|
||||||
|
specifier: ^0.503.0
|
||||||
|
version: 0.503.0
|
||||||
mssql:
|
mssql:
|
||||||
specifier: ^11.0.1
|
specifier: ^11.0.1
|
||||||
version: 11.0.1
|
version: 11.0.1
|
||||||
@ -1100,6 +1103,9 @@ packages:
|
|||||||
lodash.once@4.1.1:
|
lodash.once@4.1.1:
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
|
lucide@0.503.0:
|
||||||
|
resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -2319,6 +2325,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.once@4.1.1: {}
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
|
lucide@0.503.0: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
micromatch@4.0.8:
|
micromatch@4.0.8:
|
||||||
|
|||||||
9
src/app/inquiry/page.tsx
Normal file
9
src/app/inquiry/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import InquiryList from '@/components/inquiry/InquiryList'
|
||||||
|
|
||||||
|
export default function Inquiry() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InquiryList />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
src/app/inquiry/write/page.tsx
Normal file
9
src/app/inquiry/write/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import InquiryWriteForm from '@/components/inquiry/InquiryWriteForm'
|
||||||
|
|
||||||
|
export default function InquiryWrite() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InquiryWriteForm />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/components/inquiry/InquiryItems.tsx
Normal file
18
src/components/inquiry/InquiryItems.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
export default function InquiryItems({ inquiryData }: { inquiryData: any }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{inquiryData.map((item: any) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<div>{item.title}</div>
|
||||||
|
<div>{item.content}</div>
|
||||||
|
<div>{item.createdAt}</div>
|
||||||
|
<div>{item.writer}</div>
|
||||||
|
<div>{item.category}</div>
|
||||||
|
{item.file && <div>{item.file}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
159
src/components/inquiry/InquiryList.tsx
Normal file
159
src/components/inquiry/InquiryList.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import InquiryItems from './InquiryItems'
|
||||||
|
import InquirySearch from './InquirySearch'
|
||||||
|
|
||||||
|
const inquiryDummyData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: 'file.png',
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer',
|
||||||
|
category: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'post',
|
||||||
|
content: 'content',
|
||||||
|
file: null,
|
||||||
|
createdAt: '2024-01-01',
|
||||||
|
writer: 'writer1',
|
||||||
|
category: 'C',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function InquiryList() {
|
||||||
|
const inquiryLength = inquiryDummyData.length
|
||||||
|
const [visibleItems, setVisibleItems] = useState(5)
|
||||||
|
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
setVisibleItems((prev) => Math.min(prev + 5, inquiryLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
console.log(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredData = isMyPostsOnly
|
||||||
|
? inquiryDummyData.filter((item) => item.writer === 'writer')
|
||||||
|
: inquiryDummyData
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<InquirySearch handleSearch={handleSearch} />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="myPosts"
|
||||||
|
checked={isMyPostsOnly}
|
||||||
|
onChange={(e) => setIsMyPostsOnly(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="myPosts">
|
||||||
|
my posts
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<select>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
<span>total {inquiryDummyData.length}</span>
|
||||||
|
<InquiryItems inquiryData={filteredData.slice(0, visibleItems)} />
|
||||||
|
{visibleItems < filteredData.length && <button onClick={handleLoadMore}>more</button>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
20
src/components/inquiry/InquirySearch.tsx
Normal file
20
src/components/inquiry/InquirySearch.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
export default function InquirySearch({ handleSearch }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void }) {
|
||||||
|
const router = useRouter()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Inquiry Search</h1>
|
||||||
|
<button onClick={() => router.push('/inquiry/write')}>write 1:1 Inquiry {'>'}</button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input type="text" placeholder="Search" onChange={handleSearch} />
|
||||||
|
<button>
|
||||||
|
<Search />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
src/components/inquiry/InquiryWriteForm.tsx
Normal file
66
src/components/inquiry/InquiryWriteForm.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export interface InquiryFormData {
|
||||||
|
category: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
file: File[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InquiryWriteForm() {
|
||||||
|
const [formData, setFormData] = useState<InquiryFormData>({
|
||||||
|
category: 'A',
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
file: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(e.target.files || [])
|
||||||
|
setFormData({ ...formData, file: files })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileDelete = (fileToDelete: File) => {
|
||||||
|
setFormData({ ...formData, file: formData.file.filter((f) => f !== fileToDelete) })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="category">category</label>
|
||||||
|
<select id="category" onChange={(e) => setFormData({ ...formData, category: e.target.value })}>
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="B">B</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="title">title</label>
|
||||||
|
<input type="text" id="title" onChange={(e) => setFormData({ ...formData, title: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="content">content</label>
|
||||||
|
<textarea id="content" onChange={(e) => setFormData({ ...formData, content: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="file">file</label>
|
||||||
|
<input type="file" id="file" accept="image/*" capture="environment" onChange={handleFileChange} />
|
||||||
|
<div>
|
||||||
|
<p>file count: {formData.file.length}</p>
|
||||||
|
{formData.file.map((f) => (
|
||||||
|
<div key={f.name}>
|
||||||
|
<div>{f.name}</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => handleFileDelete(f)}>delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit">submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user