i18n 적용
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import type { ImageBlockProps } from "@/features/editor/state/editorStore";
|
||||
import { NumericPropertyControl } from "@/features/editor/components/NumericPropertyControl";
|
||||
import { ColorPickerField, TEXT_COLOR_PALETTE } from "@/features/editor/components/ColorPickerField";
|
||||
import { useAppLocale } from "@/features/i18n/LocaleProvider";
|
||||
import { getEditorImagePanelMessages } from "@/features/i18n/messages/editorImagePanel";
|
||||
|
||||
export type ImagePropertiesPanelProps = {
|
||||
imageProps: ImageBlockProps;
|
||||
@@ -11,6 +13,8 @@ export type ImagePropertiesPanelProps = {
|
||||
};
|
||||
|
||||
export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock }: ImagePropertiesPanelProps) {
|
||||
const locale = useAppLocale();
|
||||
const m = getEditorImagePanelMessages(locale);
|
||||
const source: "url" | "upload" =
|
||||
imageProps.sourceType === "asset" || (imageProps.src && imageProps.src.startsWith("/api/image/"))
|
||||
? "upload"
|
||||
@@ -20,10 +24,10 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>이미지 소스</span>
|
||||
<span>{m.imageSourceLabel}</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||
aria-label="이미지 소스"
|
||||
aria-label={m.imageSourceAria}
|
||||
value={source}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value as "url" | "upload";
|
||||
@@ -39,8 +43,8 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="url">URL</option>
|
||||
<option value="upload">파일 업로드</option>
|
||||
<option value="url">{m.imageSourceOptionUrl}</option>
|
||||
<option value="upload">{m.imageSourceOptionUpload}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -48,10 +52,10 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
{source === "url" && (
|
||||
<div className="space-y-1">
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>이미지 URL</span>
|
||||
<span>{m.imageUrlLabel}</span>
|
||||
<input
|
||||
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||
aria-label="이미지 URL"
|
||||
aria-label={m.imageUrlAria}
|
||||
value={imageProps.src}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
@@ -69,12 +73,12 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
{source === "upload" && (
|
||||
<div className="space-y-1">
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>이미지 파일 업로드</span>
|
||||
<span>{m.imageUploadLabel}</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="w-full text-[11px] text-slate-300 file:mr-2 file:rounded file:border file:border-slate-700 file:bg-slate-800 file:px-2 file:py-1 file:text-[11px] file:text-slate-100 hover:file:bg-slate-700"
|
||||
aria-label="이미지 파일 업로드"
|
||||
aria-label={m.imageUploadAria}
|
||||
onChange={async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -89,7 +93,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("이미지 업로드 실패", await response.text());
|
||||
console.error("Image upload failed", await response.text());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +106,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
assetId: data.id,
|
||||
} as any);
|
||||
} catch (error) {
|
||||
console.error("이미지 업로드 중 오류", error);
|
||||
console.error("Error while uploading image", error);
|
||||
} finally {
|
||||
event.target.value = "";
|
||||
}
|
||||
@@ -113,10 +117,10 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<label className="flex flex-col gap-1 text-xs text-slate-400">
|
||||
<span>대체 텍스트</span>
|
||||
<span>{m.altLabel}</span>
|
||||
<input
|
||||
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||
aria-label="대체 텍스트"
|
||||
aria-label={m.altAria}
|
||||
value={imageProps.alt}
|
||||
onChange={(e) => {
|
||||
updateBlock(selectedBlockId, { alt: e.target.value } as any);
|
||||
@@ -126,14 +130,14 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2 border-t border-slate-800 pt-3 text-xs text-slate-400">
|
||||
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">이미지 스타일</h4>
|
||||
<h4 className="text-[11px] font-semibold text-slate-800 dark:text-slate-200">{m.styleSectionTitle}</h4>
|
||||
|
||||
{/* 카드 배경색 */}
|
||||
<div className="space-y-1">
|
||||
<ColorPickerField
|
||||
label="카드 배경색"
|
||||
ariaLabelColorInput="이미지 카드 배경색 피커"
|
||||
ariaLabelHexInput="이미지 카드 배경색 HEX"
|
||||
label={m.cardBackgroundLabel}
|
||||
ariaLabelColorInput={m.cardBackgroundPickerAria}
|
||||
ariaLabelHexInput={m.cardBackgroundHexAria}
|
||||
value={imageProps.backgroundColorCustom ?? ""}
|
||||
onChange={(hex) => {
|
||||
const next = hex && hex.trim().length > 0 ? hex : undefined;
|
||||
@@ -145,10 +149,10 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
|
||||
{/* 정렬 */}
|
||||
<label className="flex flex-col gap-1">
|
||||
<span>정렬</span>
|
||||
<span>{m.alignLabel}</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||
aria-label="이미지 정렬"
|
||||
aria-label={m.alignAria}
|
||||
value={imageProps.align ?? "center"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
@@ -156,18 +160,18 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="left">왼쪽</option>
|
||||
<option value="center">가운데</option>
|
||||
<option value="right">오른쪽</option>
|
||||
<option value="left">{m.alignOptionLeft}</option>
|
||||
<option value="center">{m.alignOptionCenter}</option>
|
||||
<option value="right">{m.alignOptionRight}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{/* 너비 모드 */}
|
||||
<label className="flex flex-col gap-1">
|
||||
<span>너비 모드</span>
|
||||
<span>{m.widthModeLabel}</span>
|
||||
<select
|
||||
className="w-full rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-900 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||
aria-label="이미지 너비 모드"
|
||||
aria-label={m.widthModeAria}
|
||||
value={imageProps.widthMode ?? "auto"}
|
||||
onChange={(e) =>
|
||||
updateBlock(selectedBlockId, {
|
||||
@@ -175,23 +179,23 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
} as any)
|
||||
}
|
||||
>
|
||||
<option value="auto">내용에 맞춤</option>
|
||||
<option value="fixed">고정 너비 (px)</option>
|
||||
<option value="auto">{m.widthModeOptionAuto}</option>
|
||||
<option value="fixed">{m.widthModeOptionFixed}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{(imageProps.widthMode ?? "auto") === "fixed" && (
|
||||
<NumericPropertyControl
|
||||
label="고정 너비 (px)"
|
||||
unitLabel="(px)"
|
||||
label={m.fixedWidthLabel}
|
||||
unitLabel={m.fixedWidthUnitLabel}
|
||||
value={imageProps.widthPx ?? 320}
|
||||
min={40}
|
||||
max={1200}
|
||||
step={10}
|
||||
presets={[
|
||||
{ id: "sm", label: "작게", value: 240 },
|
||||
{ id: "md", label: "보통", value: 320 },
|
||||
{ id: "lg", label: "넓게", value: 480 },
|
||||
{ id: "sm", label: m.fixedWidthPresetSmall, value: 240 },
|
||||
{ id: "md", label: m.fixedWidthPresetMedium, value: 320 },
|
||||
{ id: "lg", label: m.fixedWidthPresetLarge, value: 480 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
updateBlock(selectedBlockId, {
|
||||
@@ -203,7 +207,7 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
|
||||
{/* 모서리 둥글기 */}
|
||||
<NumericPropertyControl
|
||||
label="모서리 둥글기"
|
||||
label={m.borderRadiusLabel}
|
||||
value={(() => {
|
||||
if (typeof imageProps.borderRadiusPx === "number") {
|
||||
return imageProps.borderRadiusPx;
|
||||
@@ -216,11 +220,11 @@ export function ImagePropertiesPanel({ imageProps, selectedBlockId, updateBlock
|
||||
max={200}
|
||||
step={1}
|
||||
presets={[
|
||||
{ id: "none", label: "없음", value: 0 },
|
||||
{ id: "sm", label: "작게", value: 20 },
|
||||
{ id: "md", label: "보통", value: 60 },
|
||||
{ id: "lg", label: "크게", value: 120 },
|
||||
{ id: "full", label: "완전 둥글게", value: 180 },
|
||||
{ id: "none", label: m.borderRadiusPresetNone, value: 0 },
|
||||
{ id: "sm", label: m.borderRadiusPresetSmall, value: 20 },
|
||||
{ id: "md", label: m.borderRadiusPresetMedium, value: 60 },
|
||||
{ id: "lg", label: m.borderRadiusPresetLarge, value: 120 },
|
||||
{ id: "full", label: m.borderRadiusPresetFull, value: 180 },
|
||||
]}
|
||||
onChangeValue={(v) => {
|
||||
// 0~50 범위를 토큰으로 매핑한다.
|
||||
|
||||
Reference in New Issue
Block a user