9.1 에디터(Tiptap/Quill 중 택1) 통합 o
This commit is contained in:
28
src/app/components/Editor.tsx
Normal file
28
src/app/components/Editor.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function Editor({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
if (el.innerHTML !== value) el.innerHTML = value || "";
|
||||||
|
}, [value]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
contentEditable
|
||||||
|
onInput={(e) => onChange((e.target as HTMLDivElement).innerHTML)}
|
||||||
|
data-placeholder={placeholder}
|
||||||
|
style={{
|
||||||
|
minHeight: 160,
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: 12,
|
||||||
|
}}
|
||||||
|
suppressContentEditableWarning
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useToast } from "@/app/components/ui/ToastProvider";
|
import { useToast } from "@/app/components/ui/ToastProvider";
|
||||||
import { UploadButton } from "@/app/components/UploadButton";
|
import { UploadButton } from "@/app/components/UploadButton";
|
||||||
|
import { Editor } from "@/app/components/Editor";
|
||||||
|
|
||||||
export default function EditPostPage() {
|
export default function EditPostPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
@@ -43,7 +44,7 @@ export default function EditPostPage() {
|
|||||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
<h1>글 수정</h1>
|
<h1>글 수정</h1>
|
||||||
<input placeholder="제목" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
<input placeholder="제목" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
||||||
<textarea placeholder="내용" value={form.content} onChange={(e) => setForm({ ...form, content: e.target.value })} rows={10} />
|
<Editor value={form.content} onChange={(v) => setForm({ ...form, content: v })} placeholder="내용을 입력하세요" />
|
||||||
<UploadButton onUploaded={(url) => setForm((f) => (!f ? f : { ...f, content: `${f.content}\n` }))} />
|
<UploadButton onUploaded={(url) => setForm((f) => (!f ? f : { ...f, content: `${f.content}\n` }))} />
|
||||||
<button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "저장"}</button>
|
<button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "저장"}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useToast } from "@/app/components/ui/ToastProvider";
|
import { useToast } from "@/app/components/ui/ToastProvider";
|
||||||
import { UploadButton } from "@/app/components/UploadButton";
|
import { UploadButton } from "@/app/components/UploadButton";
|
||||||
|
import { Editor } from "@/app/components/Editor";
|
||||||
|
|
||||||
export default function NewPostPage({ searchParams }: { searchParams?: { boardId?: string } }) {
|
export default function NewPostPage({ searchParams }: { searchParams?: { boardId?: string } }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -32,7 +33,7 @@ export default function NewPostPage({ searchParams }: { searchParams?: { boardId
|
|||||||
<h1>새 글</h1>
|
<h1>새 글</h1>
|
||||||
<input placeholder="boardId" value={form.boardId} onChange={(e) => setForm({ ...form, boardId: e.target.value })} />
|
<input placeholder="boardId" value={form.boardId} onChange={(e) => setForm({ ...form, boardId: e.target.value })} />
|
||||||
<input placeholder="제목" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
<input placeholder="제목" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
||||||
<textarea placeholder="내용" value={form.content} onChange={(e) => setForm({ ...form, content: e.target.value })} rows={10} />
|
<Editor value={form.content} onChange={(v) => setForm({ ...form, content: v })} placeholder="내용을 입력하세요" />
|
||||||
<UploadButton multiple onUploaded={(url) => setForm((f) => ({ ...f, content: `${f.content}\n` }))} />
|
<UploadButton multiple onUploaded={(url) => setForm((f) => ({ ...f, content: `${f.content}\n` }))} />
|
||||||
<button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "등록"}</button>
|
<button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "등록"}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
10
todolist.txt
10
todolist.txt
@@ -62,7 +62,7 @@
|
|||||||
8.8 제휴업소 요청: 요청 생성/승인/상태 관리/이력 o
|
8.8 제휴업소 요청: 요청 생성/승인/상태 관리/이력 o
|
||||||
|
|
||||||
[에디터/업로드]
|
[에디터/업로드]
|
||||||
9.1 에디터(Tiptap/Quill 중 택1) 통합
|
9.1 에디터(Tiptap/Quill 중 택1) 통합 o
|
||||||
9.2 이미지/파일 업로드 서버 처리 및 검증
|
9.2 이미지/파일 업로드 서버 처리 및 검증
|
||||||
9.3 리사이즈/웹포맷 최적화 및 용량 제한
|
9.3 리사이즈/웹포맷 최적화 및 용량 제한
|
||||||
9.4 붙여넣기/드래그 삽입, 캡션/대체텍스트
|
9.4 붙여넣기/드래그 삽입, 캡션/대체텍스트
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
12.3 XSS 콘텐츠 정제 규칙 정의/적용
|
12.3 XSS 콘텐츠 정제 규칙 정의/적용
|
||||||
12.4 비밀번호 정책/자동 로그아웃/감사 로그 설정
|
12.4 비밀번호 정책/자동 로그아웃/감사 로그 설정
|
||||||
12.5 금칙어 정책/DB 운영 절차 수립
|
12.5 금칙어 정책/DB 운영 절차 수립
|
||||||
|
화
|
||||||
[배포/운영]
|
[배포/운영]
|
||||||
13.1 환경변수 파일 체계 및 비밀키 전략
|
13.1 환경변수 파일 체계 및 비밀키 전략
|
||||||
13.2 CI 파이프라인 구성(테스트/빌드/린트)
|
13.2 CI 파이프라인 구성(테스트/빌드/린트)
|
||||||
@@ -97,5 +97,7 @@
|
|||||||
13.5 백업/복구 전략 수립
|
13.5 백업/복구 전략 수립
|
||||||
|
|
||||||
|
|
||||||
14
|
|
||||||
14.1 배포용 상태 적용
|
14 배포용 상태 적용
|
||||||
|
14.1 패스워드찾기 정상화
|
||||||
|
14.2 에디터 정상화
|
||||||
|
|||||||
Reference in New Issue
Block a user