@loword/loword-design-system 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/LDAvatar/LDEditableAvatar.d.ts +17 -0
- package/dist/components/LDAvatar/LDEditableAvatar.d.ts.map +1 -0
- package/dist/components/LDAvatar/LDEditableAvatar.js +70 -0
- package/dist/components/LDDataCalendar/LDDataCalendar.d.ts.map +1 -1
- package/dist/components/LDDataCalendar/LDDataCalendar.js +5 -4
- package/dist/components/LDDatePicker/LDDatePicker.js +1 -1
- package/dist/components/LDDateRangePicker/LDDateRangePicker.js +2 -2
- package/dist/components/LDEditor/EditorBase.d.ts.map +1 -1
- package/dist/components/LDEditor/EditorBase.js +25 -2
- package/dist/components/LDEditor/LDEditor.d.ts.map +1 -1
- package/dist/components/LDEditor/LDEditor.js +2 -1
- package/dist/components/LDFileUpload/FileUploadBase.d.ts +20 -4
- package/dist/components/LDFileUpload/FileUploadBase.d.ts.map +1 -1
- package/dist/components/LDFileUpload/FileUploadBase.js +41 -18
- package/dist/components/LDFileUpload/LDFileUpload.d.ts +19 -6
- package/dist/components/LDFileUpload/LDFileUpload.d.ts.map +1 -1
- package/dist/components/LDFileUpload/LDFileUpload.js +124 -28
- package/dist/components/LDFileUpload/LDFileUploadList.js +1 -1
- package/dist/components/LDInput/LDInput.d.ts +3 -1
- package/dist/components/LDInput/LDInput.d.ts.map +1 -1
- package/dist/components/LDInput/LDInput.js +10 -4
- package/dist/components/LDModal/LDModal.d.ts +7 -6
- package/dist/components/LDModal/LDModal.d.ts.map +1 -1
- package/dist/components/LDModal/LDModalView.d.ts +3 -3
- package/dist/components/LDModal/LDModalView.d.ts.map +1 -1
- package/dist/components/LDModal/LDModalView.js +6 -5
- package/dist/components/LDRadio/LDRadioGroup.d.ts +3 -3
- package/dist/components/LDRadio/LDRadioGroup.d.ts.map +1 -1
- package/dist/components/LDRadio/LDRadioGroup.js +1 -1
- package/dist/components/LDSelect/LDMultiSelect.js +1 -1
- package/dist/components/LDSelect/LDSelect.js +1 -1
- package/dist/components/LDTable/LDTable.js +2 -2
- package/dist/components/LDTextarea/LDTextarea.js +7 -7
- package/dist/components/LDTimePicker/LDTimePicker.js +1 -1
- package/dist/index.css +115 -20
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/file.d.ts +2 -0
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +1 -0
- package/package.json +1 -1
- package/src/styles/components/editor.css +14 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LDAvatarProps } from '../../components/LDAvatar/LDAvatar.js';
|
|
2
|
+
import { type TMimeType } from '../../constants/file.js';
|
|
3
|
+
export interface LDEditableAvatarProps extends LDAvatarProps {
|
|
4
|
+
/** 검증을 통과한 이미지 파일을 소비처에 전달합니다. 네트워크 업로드는 이 컴포넌트가 하지 않습니다. */
|
|
5
|
+
onChange: (file: File) => void | Promise<void>;
|
|
6
|
+
/** 허용 최대 파일 크기(byte)입니다. 기본값은 5MB입니다. */
|
|
7
|
+
maxSize?: number;
|
|
8
|
+
/** 허용할 이미지 MIME 타입 목록입니다. 기본값은 지원하는 전체 이미지 타입입니다. */
|
|
9
|
+
allowedExtensions?: TMimeType[];
|
|
10
|
+
/** 이미지가 있을 때 우측 상단 제거 버튼을 눌렀음을 소비처에 알립니다. 실제 `src` 정리는 소비처 몫입니다. */
|
|
11
|
+
onRemove?: () => void | Promise<void>;
|
|
12
|
+
/** true면 입력을 비활성화하고 hover 오버레이·우측 상단 배지도 표시하지 않습니다. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const LDEditableAvatar: ({ src, alt, size, className, style, onChange, onRemove, maxSize, allowedExtensions, disabled, }: LDEditableAvatarProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export default LDEditableAvatar;
|
|
17
|
+
//# sourceMappingURL=LDEditableAvatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LDEditableAvatar.d.ts","sourceRoot":"","sources":["../../../src/components/LDAvatar/LDEditableAvatar.tsx"],"names":[],"mappings":"AAKA,OAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAI9E,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAU1B,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,SAAS,EAAE,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,gBAAgB,GAAI,iGAWvB,qBAAqB,4CAkIvB,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Camera, Pencil, X } from 'lucide-react';
|
|
4
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
5
|
+
import LDAvatar from '../../components/LDAvatar/LDAvatar.js';
|
|
6
|
+
import LDToast from '../../components/LDToast/LDToast.js';
|
|
7
|
+
import RenderIcon from '../../components/RenderIcon/RenderIcon.js';
|
|
8
|
+
import Show from '../../components/Show/Show.js';
|
|
9
|
+
import { FILE_EXTENSIONS, FILE_SIZE_UNIT, SUPPORTED_IMAGE_MIME_TYPES, } from '../../constants/file.js';
|
|
10
|
+
import cn from '../../utils/cn.js';
|
|
11
|
+
import { getFileSizeUnit, validAllowedExtensions, validFileSize, validImageSize, } from '../../utils/file.js';
|
|
12
|
+
const LDEditableAvatar = ({ src, alt = 'avatar', size = 'small', className, style, onChange, onRemove, maxSize = 5 * FILE_SIZE_UNIT.mb, allowedExtensions = SUPPORTED_IMAGE_MIME_TYPES, disabled, }) => {
|
|
13
|
+
const inputId = useId();
|
|
14
|
+
const [previewSrc, setPreviewSrc] = useState(null);
|
|
15
|
+
const previewUrlRef = useRef(null);
|
|
16
|
+
// src prop이 바뀌면(업로드 완료 후 서버 URL로 전환되는 흐름) 로컬 미리보기를 버리고 src를 우선한다.
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (previewUrlRef.current) {
|
|
19
|
+
URL.revokeObjectURL(previewUrlRef.current);
|
|
20
|
+
previewUrlRef.current = null;
|
|
21
|
+
}
|
|
22
|
+
setPreviewSrc(null);
|
|
23
|
+
}, [src]);
|
|
24
|
+
// 언마운트 시 마지막 미리보기 objectURL을 해제한다.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return () => {
|
|
27
|
+
if (previewUrlRef.current)
|
|
28
|
+
URL.revokeObjectURL(previewUrlRef.current);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
const handleChange = async (event) => {
|
|
32
|
+
const file = event.target.files?.[0];
|
|
33
|
+
if (!file)
|
|
34
|
+
return;
|
|
35
|
+
if (!validAllowedExtensions(file, allowedExtensions)) {
|
|
36
|
+
const allowed = allowedExtensions
|
|
37
|
+
.map((item) => FILE_EXTENSIONS[item].toUpperCase())
|
|
38
|
+
.join(', ');
|
|
39
|
+
LDToast.error(`지원하지 않는 파일 형식입니다. ${allowed} 파일만 업로드 가능합니다.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!validFileSize(file, maxSize)) {
|
|
43
|
+
LDToast.error(`파일 크기가 너무 큽니다. ${getFileSizeUnit(maxSize)} 이하 파일을 선택해주세요.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!(await validImageSize(file))) {
|
|
47
|
+
LDToast.error('이미지 해상도가 너무 큽니다. 더 작은 크기의 이미지를 선택해주세요.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (previewUrlRef.current)
|
|
51
|
+
URL.revokeObjectURL(previewUrlRef.current);
|
|
52
|
+
const nextPreviewUrl = URL.createObjectURL(file);
|
|
53
|
+
previewUrlRef.current = nextPreviewUrl;
|
|
54
|
+
setPreviewSrc(nextPreviewUrl);
|
|
55
|
+
await onChange(file);
|
|
56
|
+
};
|
|
57
|
+
const handleRemove = async () => {
|
|
58
|
+
if (previewUrlRef.current) {
|
|
59
|
+
URL.revokeObjectURL(previewUrlRef.current);
|
|
60
|
+
previewUrlRef.current = null;
|
|
61
|
+
}
|
|
62
|
+
setPreviewSrc(null);
|
|
63
|
+
await onRemove?.();
|
|
64
|
+
};
|
|
65
|
+
const hasImage = Boolean(previewSrc ?? src);
|
|
66
|
+
return (_jsxs("div", { className: "relative inline-block", children: [_jsx(LDAvatar, { src: previewSrc ?? src, alt: alt, size: size, className: className, style: style }), _jsxs("label", { htmlFor: inputId, className: cn('group absolute inset-0 rounded-full', disabled ? 'cursor-default' : 'cursor-pointer'), children: [_jsx("input", { id: inputId, type: "file", className: "peer sr-only", accept: allowedExtensions.join(','), disabled: disabled, "aria-label": `${alt} 이미지 변경`, onClick: (event) => {
|
|
67
|
+
event.currentTarget.value = '';
|
|
68
|
+
}, onChange: handleChange }), _jsx(Show, { when: !disabled, children: _jsx("span", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-mono900/50 opacity-0 ld-transition group-hover:opacity-100 peer-focus-visible:opacity-100", children: _jsx(RenderIcon, { className: "size-1/2 text-white", children: _jsx(Camera, {}) }) }) }), _jsx(Show, { when: !disabled && !hasImage, children: _jsx("span", { "aria-hidden": true, className: "absolute -top-0.5 -right-0.5 flex size-4.5 items-center justify-center rounded-full bg-blue300 ld-transition group-hover:bg-blue400", children: _jsx(RenderIcon, { className: "size-2.5 text-white", children: _jsx(Pencil, {}) }) }) })] }), _jsx(Show, { when: !disabled && hasImage, children: _jsx("button", { type: "button", "aria-label": `${alt} 이미지 제거`, onClick: handleRemove, className: "absolute -top-0.5 -right-0.5 flex size-4.5 cursor-pointer items-center justify-center rounded-full bg-danger600 ld-transition hover:bg-danger700", children: _jsx(RenderIcon, { className: "size-2.5 text-white", children: _jsx(X, {}) }) }) })] }));
|
|
69
|
+
};
|
|
70
|
+
export default LDEditableAvatar;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDDataCalendar.d.ts","sourceRoot":"","sources":["../../../src/components/LDDataCalendar/LDDataCalendar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,OAAO,kBAAkB,MAAM,gDAAgD,CAAC;
|
|
1
|
+
{"version":3,"file":"LDDataCalendar.d.ts","sourceRoot":"","sources":["../../../src/components/LDDataCalendar/LDDataCalendar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,OAAO,kBAAkB,MAAM,gDAAgD,CAAC;AAIhF,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc;IACd,YAAY,CAAC,EAAE,KAAK,CAAC;IACrB,gBAAgB;IAChB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACvC,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe;IACf,mBAAmB,CAAC,EAAE,KAAK,CAAC;IAC5B,iBAAiB;IACjB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,qBAAqB;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,wDAAwD;IACxD,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,KAAK,SAAS,CAAC;IAC5D,mDAAmD;IACnD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC;CAClD;AAED,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,kBAAkB,CAAC,IAAI,CAC/B,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AAC9D,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,kBAAkB,CAAC,IAAI,CAC/B,CAAC;AACF,MAAM,MAAM,wBAAwB,GAAG,cAAc,CACnD,OAAO,kBAAkB,CAAC,KAAK,CAChC,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,kBAAkB,CAAC,IAAI,CAC/B,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,kBAAkB,CAAC,IAAI,CAC/B,CAAC;AACF,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,kBAAkB,CAAC,IAAI,CAC/B,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG,cAAc,CACpD,OAAO,kBAAkB,CAAC,MAAM,CACjC,CAAC;AA8FF;;;GAGG;AACH,QAAA,MAAM,cAAc;qIAUjB,mBAAmB;oCAtGiB,uBAAuB;sCAQrB,yBAAyB;8CAQjB,uBAAuB;qCAShC,wBAAwB;8CAWf,uBAAuB;2CAS1B,uBAAuB;mDAelE,uBAAuB;sCAkBe,yBAAyB;CAgDjE,CAAC;AAWF,eAAe,cAAc,CAAC"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
4
|
import LDDataCalendarBase from '../../components/LDDataCalendar/LDDataCalendarBase.js';
|
|
5
|
+
import Show from '../../components/Show/Show.js';
|
|
5
6
|
import cn from '../../utils/cn.js';
|
|
6
7
|
const navButtonClass = 'flex items-center justify-center rounded-full size-[30px] bg-neutral100 hover:bg-mono400 text-neutral500 hover:text-neutral900 ld-transition cursor-pointer';
|
|
7
8
|
/** 달력의 월과 선택 상태를 관리하는 Styled Compound 루트입니다. */
|
|
@@ -9,15 +10,15 @@ const Root = ({ className, ...props }) => (_jsx(LDDataCalendarBase.Root, { class
|
|
|
9
10
|
/** 월 이동 버튼과 현재 월 제목을 배치하는 헤더입니다. */
|
|
10
11
|
const Header = ({ className, ...props }) => (_jsx("div", { className: cn('mb-md flex items-center justify-between', className), ...props }));
|
|
11
12
|
/** 이전 달로 이동하는 버튼입니다. 기본 접근성 이름은 `이전 달`입니다. */
|
|
12
|
-
const Prev = ({ children, className, ...props }) => (_jsx(LDDataCalendarBase.Prev, { className: cn(navButtonClass, className), ...props, children:
|
|
13
|
+
const Prev = ({ children, className, ...props }) => (_jsx(LDDataCalendarBase.Prev, { className: cn(navButtonClass, className), ...props, children: _jsx(Show, { when: children, fallback: _jsx(ChevronLeft, { size: 16, "aria-hidden": true }), children: children }) }));
|
|
13
14
|
/** 현재 표시 중인 월 제목입니다. */
|
|
14
15
|
const Title = ({ className, ...props }) => (_jsx(LDDataCalendarBase.Title, { className: cn('flex justify-center display32 bold mobile:display20', className), ...props }));
|
|
15
16
|
/** 다음 달로 이동하는 버튼입니다. 기본 접근성 이름은 `다음 달`입니다. */
|
|
16
|
-
const Next = ({ children, className, ...props }) => (_jsx(LDDataCalendarBase.Next, { className: cn(navButtonClass, className), ...props, children:
|
|
17
|
+
const Next = ({ children, className, ...props }) => (_jsx(LDDataCalendarBase.Next, { className: cn(navButtonClass, className), ...props, children: _jsx(Show, { when: children, fallback: _jsx(ChevronRight, { size: 16, "aria-hidden": true }), children: children }) }));
|
|
17
18
|
/** 요일과 날짜 셀을 7열로 렌더링하는 그리드입니다. */
|
|
18
|
-
const Grid = ({ weekdayClassName, ...props }) => (_jsx(LDDataCalendarBase.Grid, { weekdayClassName: cn('py-
|
|
19
|
+
const Grid = ({ weekdayClassName, ...props }) => (_jsx(LDDataCalendarBase.Grid, { weekdayClassName: cn('px-2.5 py-sm display16 bold text-mono500 mobile:p-xs mobile:display14', weekdayClassName), ...props }));
|
|
19
20
|
/** 날짜와 해당 날짜의 데이터를 표시하는 셀입니다. */
|
|
20
|
-
const Cell = ({ className, dateClassName, ...props }) => (_jsx(LDDataCalendarBase.Cell, { className: cn('flex min-h-25 flex-col gap-
|
|
21
|
+
const Cell = ({ className, dateClassName, ...props }) => (_jsx(LDDataCalendarBase.Cell, { className: cn('flex min-h-25 flex-col gap-0.5 p-2.5 display14 text-neutral500 ld-transition data-outside:opacity-40 mobile:min-h-21 mobile:p-xs mobile:display12', 'data-selectable:cursor-pointer data-selectable:rounded-[5px] data-selectable:hover:bg-mono100', 'data-[selected=true]:bg-tintBlue data-[selected=true]:hover:bg-tintBlue', '*:truncate', className), dateClassName: cn('self-start pb-1.5 display16 leading-none font-normal text-black data-weekend:text-danger600 mobile:display14', dateClassName), ...props }));
|
|
21
22
|
/** 선택한 날짜의 상세 내용을 표시하는 모바일 영역입니다. */
|
|
22
23
|
const Detail = ({ className, ...props }) => (_jsx(LDDataCalendarBase.Detail, { className: cn('mt-md hidden rounded-[10px] bg-mono50 p-md mobile:block', className), ...props }));
|
|
23
24
|
/**
|
|
@@ -102,6 +102,6 @@ const LDDatePicker = ({ variant = 'filled', color = 'neutral', value, defaultVal
|
|
|
102
102
|
setSelectedDate(value);
|
|
103
103
|
setSelectedTime(toTimeString(value));
|
|
104
104
|
}, [open, value]);
|
|
105
|
-
return (_jsx("div", { className: cn(block ? 'w-full' : 'w-auto', className), children: _jsxs(PopoverBase.Root, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { style: style, className: cn('relative h-9.5 w-full cursor-pointer rounded-[5px] px-4 display14', 'flex flex-row items-center justify-between gap-4', 'bold outline-2 outline-transparent ld-transition', variantColorClasses[variant][color], disabled && 'cursor-not-allowed opacity-50', triggerClassName), children: [_jsx("span", { "aria-hidden": true, className: cn('truncate', !displayDate && 'text-
|
|
105
|
+
return (_jsx("div", { className: cn(block ? 'w-full' : 'w-auto', className), children: _jsxs(PopoverBase.Root, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { style: style, className: cn('relative h-9.5 w-full cursor-pointer rounded-[5px] px-4 display14', 'flex flex-row items-center justify-between gap-4', 'bold outline-2 outline-transparent ld-transition', variantColorClasses[variant][color], disabled && 'cursor-not-allowed opacity-50', triggerClassName), children: [_jsx("span", { "aria-hidden": true, className: cn('truncate', !displayDate && 'text-neutral300'), children: displayDate ? formattedDate : placeholderText }), _jsx(PopoverBase.Trigger, { "aria-label": displayDate ? formattedDate : placeholderText, disabled: disabled, className: cn('absolute inset-0 rounded-[5px] outline-none', disabled ? 'cursor-not-allowed' : 'cursor-pointer') }), _jsxs("div", { className: "pointer-events-none relative flex shrink-0 items-center gap-xs", children: [showRotate && (_jsx("button", { type: "button", "aria-label": "\uCD08\uAE30\uD654", disabled: disabled, onClick: handleReset, className: "pointer-events-auto flex cursor-pointer items-center text-mono400 ld-transition disabled:cursor-not-allowed", children: _jsx(RotateCw, { size: 15 }) })), trailingIcon] })] }), _jsx(PopoverBase.Content, { className: "rounded-md border border-neutral200 bg-white p-0 shadow-md", children: _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex max-h-90 flex-row", children: [_jsx(LDCalendar, { captionLayout: "dropdown", mode: "single", locale: ko, selected: selectedDate, onSelect: handleSelect, disabled: isDateDisabled, colorSet: colorSet }), isExistTime && (_jsx(CalendarTimePicker, { value: selectedTime, onChange: handleSelectedTime, colorSet: colorSet, className: "shrink-0" }))] }), _jsxs("div", { className: "flex items-center justify-end gap-sm border-t border-neutral100 py-xs px-sm", children: [_jsx(LDButton, { variant: "outline", color: "neutral", onClick: handleCancel, children: "\uCDE8\uC18C" }), _jsx(LDButton, { onClick: handleConfirm, children: "\uD655\uC778" })] })] }) })] }) }));
|
|
106
106
|
};
|
|
107
107
|
export default LDDatePicker;
|
|
@@ -85,10 +85,10 @@ const LDDateRangePicker = ({ variant = 'filled', value, onChange, disabled = fal
|
|
|
85
85
|
})();
|
|
86
86
|
const renderDateRange = () => {
|
|
87
87
|
if (!displayRange?.from) {
|
|
88
|
-
return (_jsx("span", { className: "whitespace-nowrap text-
|
|
88
|
+
return (_jsx("span", { className: "whitespace-nowrap text-neutral300", children: placeholderText }));
|
|
89
89
|
}
|
|
90
90
|
if (!displayRange.to) {
|
|
91
|
-
return (_jsxs("span", { className: "text-
|
|
91
|
+
return (_jsxs("span", { className: "text-neutral300", children: [dayjs(displayRange.from).format(displayFmt), " ~ ", placeholderFmt] }));
|
|
92
92
|
}
|
|
93
93
|
return (_jsxs(_Fragment, { children: [dayjs(displayRange.from).format(displayFmt), " ~", ' ', dayjs(displayRange.to).format(displayFmt)] }));
|
|
94
94
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/EditorBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"EditorBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/EditorBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,UAAU,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAKf,UAAU,iBAAiB;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAID,eAAO,MAAM,gBAAgB,yBAS5B,CAAC;AAGF,UAAU,SAAS;IACjB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAwCD,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAQD,UAAU,YAAY;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAeD,UAAU,kBAAkB;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0BD,UAAU,oBAAoB;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC,CAAC;CACrE;AA0ED,UAAU,aAAa;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAgBD,eAAO,MAAM,UAAU;oEA9LpB,SAAS;6BAoCoB,YAAY;wCAWE,YAAY;yFA2BvD,kBAAkB;sDA4BlB,oBAAoB;;mDA0E+B,aAAa;CAmBlE,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { EditorContent, useEditor, } from '@tiptap/react';
|
|
4
|
-
import { createContext, useContext, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
5
|
import cn from '../../utils/cn.js';
|
|
6
6
|
const EditorContext = createContext(null);
|
|
7
7
|
export const useEditorContext = () => {
|
|
@@ -51,7 +51,10 @@ const ToolbarButton = ({ disabled, onClick, className, children, 'aria-label': a
|
|
|
51
51
|
};
|
|
52
52
|
const ToolbarDropdown = ({ trigger, className, children, }) => {
|
|
53
53
|
const [open, setOpen] = useState(false);
|
|
54
|
+
// 패널 기준 위치는 트리거 왼쪽. 툴바(=에디터 폭) 오른쪽을 넘는 만큼만 왼쪽으로 밀어 넣는다.
|
|
55
|
+
const [shift, setShift] = useState(0);
|
|
54
56
|
const ref = useRef(null);
|
|
57
|
+
const panelRef = useRef(null);
|
|
55
58
|
const toggle = () => setOpen((prev) => !prev);
|
|
56
59
|
const close = () => setOpen(false);
|
|
57
60
|
useEffect(() => {
|
|
@@ -63,7 +66,27 @@ const ToolbarDropdown = ({ trigger, className, children, }) => {
|
|
|
63
66
|
document.addEventListener('mousedown', handler);
|
|
64
67
|
return () => document.removeEventListener('mousedown', handler);
|
|
65
68
|
}, []);
|
|
66
|
-
|
|
69
|
+
// paint 전에 위치를 확정해야 열리는 첫 프레임이 튀지 않는다.
|
|
70
|
+
// Root가 editor 생성 전엔 children을 렌더하지 않으므로 서버에서는 실행되지 않는다.
|
|
71
|
+
useLayoutEffect(() => {
|
|
72
|
+
if (!open)
|
|
73
|
+
return;
|
|
74
|
+
const measure = () => {
|
|
75
|
+
const bounds = ref.current?.parentElement;
|
|
76
|
+
const panel = panelRef.current;
|
|
77
|
+
if (!ref.current || !bounds || !panel)
|
|
78
|
+
return;
|
|
79
|
+
const boundsRect = bounds.getBoundingClientRect();
|
|
80
|
+
const triggerRect = ref.current.getBoundingClientRect();
|
|
81
|
+
const overflow = triggerRect.left + panel.offsetWidth - boundsRect.right;
|
|
82
|
+
// 왼쪽 경계도 넘지 않도록 밀 수 있는 양을 제한한다.
|
|
83
|
+
setShift(Math.max(0, Math.min(overflow, triggerRect.left - boundsRect.left)));
|
|
84
|
+
};
|
|
85
|
+
measure();
|
|
86
|
+
window.addEventListener('resize', measure);
|
|
87
|
+
return () => window.removeEventListener('resize', measure);
|
|
88
|
+
}, [open]);
|
|
89
|
+
return (_jsxs("div", { ref: ref, className: "relative", children: [trigger({ open, toggle }), open && (_jsx("div", { ref: panelRef, style: { left: -shift }, className: cn('absolute top-full z-50 mt-1', className), children: typeof children === 'function' ? children({ close }) : children }))] }));
|
|
67
90
|
};
|
|
68
91
|
const Toolbar = Object.assign(ToolbarRoot, {
|
|
69
92
|
LDButton: ToolbarButton,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"AA4EA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE;QAC7B,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9B;AAED,QAAA,MAAM,QAAQ,GAAI,sDAKf,aAAa,4CAYf,CAAC;
|
|
1
|
+
{"version":3,"file":"LDEditor.d.ts","sourceRoot":"","sources":["../../../src/components/LDEditor/LDEditor.tsx"],"names":[],"mappings":"AA4EA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE;QAC7B,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9B;AAED,QAAA,MAAM,QAAQ,GAAI,sDAKf,aAAa,4CAYf,CAAC;AAyjBF,eAAe,QAAQ,CAAC"}
|
|
@@ -77,6 +77,7 @@ const EditorToolbar = ({ handleImageUpload, }) => {
|
|
|
77
77
|
canUndo: editor.can().undo(),
|
|
78
78
|
canRedo: editor.can().redo(),
|
|
79
79
|
isLink: editor.isActive('link'),
|
|
80
|
+
hasSelection: !editor.state.selection.empty,
|
|
80
81
|
};
|
|
81
82
|
},
|
|
82
83
|
});
|
|
@@ -148,7 +149,7 @@ const EditorToolbar = ({ handleImageUpload, }) => {
|
|
|
148
149
|
justify: '양쪽 정렬',
|
|
149
150
|
}[align];
|
|
150
151
|
return (_jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().setTextAlign(align).run(), className: cn(iconBtn, state.align[align] && activeBtn), "aria-label": alignLabel, children: _jsx(Icon, { className: "size-5" }) }, align));
|
|
151
|
-
})] })), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), className: cn(iconBtn, state.isBulletList && activeBtn), "aria-label": "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", children: _jsx(List, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), className: cn(iconBtn, state.isOrderedList && activeBtn), "aria-label": "\uBC88\uD638 \uBAA9\uB85D", children: _jsx(ListOrdered, { className: "size-5" }) }), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), className: iconBtn, "aria-label": "\uB418\uB3CC\uB9AC\uAE30", children: _jsx(Undo, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), className: iconBtn, "aria-label": "\uB2E4\uC2DC \uC2E4\uD589", children: _jsx(Redo, { className: "size-5" }) }), !isMobile && (_jsxs(_Fragment, { children: [_jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-70 rounded-lg border border-neutral200 bg-white p-3 shadow-lg", trigger: ({ toggle }) => (_jsx(EditorBase.Toolbar.LDButton, { onClick: () => {
|
|
152
|
+
})] })), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), className: cn(iconBtn, state.isBulletList && activeBtn), "aria-label": "\uAE00\uBA38\uB9AC \uAE30\uD638 \uBAA9\uB85D", children: _jsx(List, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), className: cn(iconBtn, state.isOrderedList && activeBtn), "aria-label": "\uBC88\uD638 \uBAA9\uB85D", children: _jsx(ListOrdered, { className: "size-5" }) }), _jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), className: iconBtn, "aria-label": "\uB418\uB3CC\uB9AC\uAE30", children: _jsx(Undo, { className: "size-5" }) }), _jsx(EditorBase.Toolbar.LDButton, { disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), className: iconBtn, "aria-label": "\uB2E4\uC2DC \uC2E4\uD589", children: _jsx(Redo, { className: "size-5" }) }), !isMobile && (_jsxs(_Fragment, { children: [_jsx(LDDivider, {}), _jsx(EditorBase.Toolbar.Dropdown, { className: "min-w-70 rounded-lg border border-neutral200 bg-white p-3 shadow-lg", trigger: ({ toggle }) => (_jsx(EditorBase.Toolbar.LDButton, { disabled: !state.hasSelection, onClick: () => {
|
|
152
153
|
setLinkUrl(editor.getAttributes('link').href || '');
|
|
153
154
|
toggle();
|
|
154
155
|
}, className: cn(iconBtn, state.isLink && activeBtn), "aria-label": "\uB9C1\uD06C", children: _jsx(Link2, { className: "size-5" }) })), children: ({ close }) => {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type Dispatch, type HTMLAttributes, type InputHTMLAttributes, type ReactNode, type Ref, type SetStateAction } from 'react';
|
|
2
2
|
import { type TMimeType } from '../../constants/file.js';
|
|
3
3
|
import { type IFile } from '../../utils/file.js';
|
|
4
|
+
/** 인풋마다 다르게 줄 수 있는 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
5
|
+
export interface FileUploadOptions {
|
|
6
|
+
maxSize?: number;
|
|
7
|
+
allowedExtensions?: TMimeType[];
|
|
8
|
+
maxFileCount?: number;
|
|
9
|
+
}
|
|
4
10
|
interface FileUploadContextValue {
|
|
5
11
|
recommendedSize?: {
|
|
6
12
|
width: number;
|
|
@@ -9,13 +15,17 @@ interface FileUploadContextValue {
|
|
|
9
15
|
maxSize: number;
|
|
10
16
|
allowedExtensions: TMimeType[];
|
|
11
17
|
fileList: IFile[];
|
|
18
|
+
linkList: string[];
|
|
12
19
|
maxFileCount: number;
|
|
13
20
|
disabled?: boolean;
|
|
14
21
|
readOnly?: boolean;
|
|
15
22
|
locked: boolean;
|
|
16
|
-
addFiles: (files: File[]) => Promise<void>;
|
|
17
|
-
replaceFile: (file: File, index: number) => Promise<void>;
|
|
23
|
+
addFiles: (files: File[], options?: FileUploadOptions) => Promise<void>;
|
|
24
|
+
replaceFile: (file: File, index: number, options?: FileUploadOptions) => Promise<void>;
|
|
18
25
|
deleteFile: (index: number) => Promise<void>;
|
|
26
|
+
addLink: () => Promise<void>;
|
|
27
|
+
changeLink: (index: number, value: string) => Promise<void>;
|
|
28
|
+
deleteLink: (index: number) => Promise<void>;
|
|
19
29
|
}
|
|
20
30
|
export declare const useFileUploadContext: () => FileUploadContextValue;
|
|
21
31
|
export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
@@ -27,6 +37,10 @@ export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElem
|
|
|
27
37
|
allowedExtensions?: TMimeType[];
|
|
28
38
|
fileList: IFile[];
|
|
29
39
|
setFileList: Dispatch<SetStateAction<IFile[]>>;
|
|
40
|
+
/** 첨부된 링크 목록입니다. `Input type="link"`·`LinkList`를 쓸 때 넘깁니다. */
|
|
41
|
+
linkList?: string[];
|
|
42
|
+
setLinkList?: Dispatch<SetStateAction<string[]>>;
|
|
43
|
+
onChangeLink?: (links: string[]) => void | Promise<void>;
|
|
30
44
|
maxFileCount: number;
|
|
31
45
|
onChangeFile: (files: IFile[]) => void | Promise<void>;
|
|
32
46
|
onDeleteFile: (file: IFile) => Promise<boolean>;
|
|
@@ -37,12 +51,14 @@ export interface FileUploadBaseRootProps extends Omit<HTMLAttributes<HTMLDivElem
|
|
|
37
51
|
}
|
|
38
52
|
export interface FileUploadBaseInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'multiple' | 'onChange' | 'type'> {
|
|
39
53
|
replaceIndex?: number;
|
|
54
|
+
/** 이 인풋에만 적용할 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
55
|
+
options?: FileUploadOptions;
|
|
40
56
|
onChange?: InputHTMLAttributes<HTMLInputElement>['onChange'];
|
|
41
57
|
ref?: Ref<HTMLInputElement>;
|
|
42
58
|
}
|
|
43
59
|
export declare const FileUploadBase: {
|
|
44
|
-
Root: ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }: FileUploadBaseRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
45
|
-
Input: ({ replaceIndex, onClick, onChange, disabled, ref, ...props }: FileUploadBaseInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
60
|
+
Root: ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, linkList: linkListProp, setLinkList, onChangeLink, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }: FileUploadBaseRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
61
|
+
Input: ({ replaceIndex, options, onClick, onChange, disabled, ref, ...props }: FileUploadBaseInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
46
62
|
};
|
|
47
63
|
export default FileUploadBase;
|
|
48
64
|
//# sourceMappingURL=FileUploadBase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUploadBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/FileUploadBase.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,GAAG,EACR,KAAK,cAAc,EAEpB,MAAM,OAAO,CAAC;AAGf,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAGL,KAAK,KAAK,EAKX,MAAM,cAAc,CAAC;AAEtB,UAAU,sBAAsB;IAC9B,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,SAAS,EAAE,CAAC;IAC/B,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"FileUploadBase.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/FileUploadBase.tsx"],"names":[],"mappings":"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"}
|
|
@@ -12,29 +12,38 @@ export const useFileUploadContext = () => {
|
|
|
12
12
|
}
|
|
13
13
|
return context;
|
|
14
14
|
};
|
|
15
|
-
const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }) => {
|
|
15
|
+
const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsProp, maxFileCount, fileList, setFileList, linkList: linkListProp, setLinkList, onChangeLink, onChangeFile, onDeleteFile, disabled, readOnly, children, ref, ...props }) => {
|
|
16
16
|
const allowedExtensions = allowedExtensionsProp?.length
|
|
17
17
|
? allowedExtensionsProp
|
|
18
18
|
: SUPPORTED_FILE_MIME_TYPES;
|
|
19
19
|
const locked = Boolean(disabled || readOnly);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const linkList = linkListProp ?? [];
|
|
21
|
+
// 인풋마다 허용 조건이 다를 수 있어, Root 값 위에 인풋이 준 옵션을 덮어쓴다.
|
|
22
|
+
const resolveOptions = (options) => ({
|
|
23
|
+
maxSize: options?.maxSize ?? maxSize,
|
|
24
|
+
allowedExtensions: options?.allowedExtensions?.length
|
|
25
|
+
? options.allowedExtensions
|
|
26
|
+
: allowedExtensions,
|
|
27
|
+
maxFileCount: options?.maxFileCount ?? maxFileCount,
|
|
28
|
+
});
|
|
29
|
+
const validateFile = async (file, resolved) => {
|
|
30
|
+
if (!validAllowedExtensions(file, resolved.allowedExtensions)) {
|
|
31
|
+
const allowed = resolved.allowedExtensions
|
|
23
32
|
.map((item) => FILE_EXTENSIONS[item].toUpperCase())
|
|
24
33
|
.join(', ');
|
|
25
34
|
return `지원하지 않는 파일 형식입니다. ${allowed} 파일만 업로드 가능합니다.`;
|
|
26
35
|
}
|
|
27
|
-
if (!validFileSize(file, maxSize)) {
|
|
28
|
-
return `파일 크기가 너무 큽니다. ${getFileSizeUnit(maxSize)} 이하 파일을 선택해주세요.`;
|
|
36
|
+
if (!validFileSize(file, resolved.maxSize)) {
|
|
37
|
+
return `파일 크기가 너무 큽니다. ${getFileSizeUnit(resolved.maxSize)} 이하 파일을 선택해주세요.`;
|
|
29
38
|
}
|
|
30
39
|
if (isImage(file.name) && !(await validImageSize(file))) {
|
|
31
40
|
return '이미지 해상도가 너무 큽니다. 더 작은 크기의 이미지를 선택해주세요.';
|
|
32
41
|
}
|
|
33
42
|
return null;
|
|
34
43
|
};
|
|
35
|
-
const convertFiles = async (files) => {
|
|
44
|
+
const convertFiles = async (files, resolved) => {
|
|
36
45
|
for (const file of files) {
|
|
37
|
-
const error = await validateFile(file);
|
|
46
|
+
const error = await validateFile(file, resolved);
|
|
38
47
|
if (error) {
|
|
39
48
|
LDToast.error(error);
|
|
40
49
|
return null;
|
|
@@ -50,20 +59,21 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
50
59
|
return null;
|
|
51
60
|
}
|
|
52
61
|
};
|
|
53
|
-
const addFiles = async (files) => {
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
const addFiles = async (files, options) => {
|
|
63
|
+
const resolved = resolveOptions(options);
|
|
64
|
+
if (fileList.length + files.length > resolved.maxFileCount) {
|
|
65
|
+
LDToast.error(`최대 ${resolved.maxFileCount}개까지 업로드할 수 있습니다.`);
|
|
56
66
|
return;
|
|
57
67
|
}
|
|
58
|
-
const converted = await convertFiles(files);
|
|
68
|
+
const converted = await convertFiles(files, resolved);
|
|
59
69
|
if (!converted)
|
|
60
70
|
return;
|
|
61
71
|
const next = [...fileList, ...converted];
|
|
62
72
|
setFileList(next);
|
|
63
73
|
await onChangeFile(next);
|
|
64
74
|
};
|
|
65
|
-
const replaceFile = async (file, index) => {
|
|
66
|
-
const [converted] = (await convertFiles([file])) ?? [];
|
|
75
|
+
const replaceFile = async (file, index, options) => {
|
|
76
|
+
const [converted] = (await convertFiles([file], resolveOptions(options))) ?? [];
|
|
67
77
|
if (!converted)
|
|
68
78
|
return;
|
|
69
79
|
const next = fileList.map((item, itemIndex) => itemIndex === index ? converted : item);
|
|
@@ -85,11 +95,20 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
85
95
|
LDToast.error('파일 삭제 중 문제가 발생했습니다. 잠시후 다시 시도해주세요.');
|
|
86
96
|
}
|
|
87
97
|
};
|
|
98
|
+
// 링크는 파일이 아니라 검증·변환 대상이 아니므로 목록 갱신만 하고 onChangeLink로 알린다.
|
|
99
|
+
const applyLinks = async (next) => {
|
|
100
|
+
setLinkList?.(next);
|
|
101
|
+
await onChangeLink?.(next);
|
|
102
|
+
};
|
|
103
|
+
const addLink = () => applyLinks([...linkList, '']);
|
|
104
|
+
const changeLink = (index, value) => applyLinks(linkList.map((item, itemIndex) => (itemIndex === index ? value : item)));
|
|
105
|
+
const deleteLink = (index) => applyLinks(linkList.filter((_, itemIndex) => itemIndex !== index));
|
|
88
106
|
return (_jsx(FileUploadContext, { value: {
|
|
89
107
|
recommendedSize,
|
|
90
108
|
maxSize,
|
|
91
109
|
allowedExtensions,
|
|
92
110
|
fileList,
|
|
111
|
+
linkList,
|
|
93
112
|
maxFileCount,
|
|
94
113
|
disabled,
|
|
95
114
|
readOnly,
|
|
@@ -97,11 +116,15 @@ const Root = ({ recommendedSize, maxSize, allowedExtensions: allowedExtensionsPr
|
|
|
97
116
|
addFiles,
|
|
98
117
|
replaceFile,
|
|
99
118
|
deleteFile,
|
|
119
|
+
addLink,
|
|
120
|
+
changeLink,
|
|
121
|
+
deleteLink,
|
|
100
122
|
}, children: _jsx("div", { ref: ref, ...props, children: children }) }));
|
|
101
123
|
};
|
|
102
|
-
const Input = ({ replaceIndex, onClick, onChange, disabled, ref, ...props }) => {
|
|
124
|
+
const Input = ({ replaceIndex, options, onClick, onChange, disabled, ref, ...props }) => {
|
|
103
125
|
const { maxFileCount, locked, addFiles, replaceFile } = useFileUploadContext();
|
|
104
|
-
|
|
126
|
+
const resolvedMaxFileCount = options?.maxFileCount ?? maxFileCount;
|
|
127
|
+
return (_jsx("input", { ref: ref, ...props, type: "file", multiple: replaceIndex === undefined && resolvedMaxFileCount > 1, disabled: locked || disabled, onClick: (event) => {
|
|
105
128
|
onClick?.(event);
|
|
106
129
|
if (!event.defaultPrevented)
|
|
107
130
|
event.currentTarget.value = '';
|
|
@@ -113,9 +136,9 @@ const Input = ({ replaceIndex, onClick, onChange, disabled, ref, ...props }) =>
|
|
|
113
136
|
if (!files.length)
|
|
114
137
|
return;
|
|
115
138
|
if (replaceIndex === undefined)
|
|
116
|
-
await addFiles(files);
|
|
139
|
+
await addFiles(files, options);
|
|
117
140
|
else if (files[0])
|
|
118
|
-
await replaceFile(files[0], replaceIndex);
|
|
141
|
+
await replaceFile(files[0], replaceIndex, options);
|
|
119
142
|
} }));
|
|
120
143
|
};
|
|
121
144
|
export const FileUploadBase = { Root, Input };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type HTMLAttributes } from 'react';
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { type LDButtonProps } from '../../components/LDButton/LDButton.js';
|
|
3
|
-
import { type FileUploadBaseRootProps } from '../../components/LDFileUpload/FileUploadBase.js';
|
|
3
|
+
import { type FileUploadBaseRootProps, type FileUploadOptions } from '../../components/LDFileUpload/FileUploadBase.js';
|
|
4
4
|
export interface LDFileUploadClassNames {
|
|
5
5
|
root?: string;
|
|
6
6
|
input?: string;
|
|
@@ -12,23 +12,36 @@ export interface LDFileUploadRootProps extends FileUploadBaseRootProps {
|
|
|
12
12
|
classNames?: LDFileUploadClassNames;
|
|
13
13
|
}
|
|
14
14
|
export interface LDFileUploadInputProps extends Omit<LDButtonProps<'button'>, 'type'> {
|
|
15
|
-
type?: 'default' | 'image';
|
|
15
|
+
type?: 'default' | 'attachment' | 'image' | 'link';
|
|
16
16
|
label?: string;
|
|
17
|
+
/** 라벨 앞 아이콘입니다. `type="default"`에서는 `leftIcon`을 씁니다. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** 이 인풋에만 적용할 파일 허용 조건입니다. 생략한 항목은 Root 값을 씁니다. */
|
|
20
|
+
options?: FileUploadOptions;
|
|
17
21
|
}
|
|
18
22
|
export interface LDFileUploadCardListProps {
|
|
19
23
|
type?: 'default' | 'image';
|
|
24
|
+
/** 카드에 업로드 시각 라벨을 표시합니다. `uploadedAt`이 있는 파일만 표시됩니다. */
|
|
25
|
+
isExistUploadedAt?: boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LDFileUploadLinkListProps {
|
|
29
|
+
placeholder?: string;
|
|
20
30
|
className?: string;
|
|
21
31
|
}
|
|
22
32
|
export type LDFileUploadDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
23
33
|
export interface LDFileUploadProps extends Omit<LDFileUploadRootProps, 'children' | 'classNames' | 'maxFileCount'> {
|
|
24
34
|
isExistDescription?: boolean;
|
|
35
|
+
/** 카드에 업로드 시각 라벨을 표시합니다. */
|
|
36
|
+
isExistUploadedAt?: boolean;
|
|
25
37
|
}
|
|
26
38
|
/** 한 개의 일반 파일을 업로드하는 기본 조합입니다. */
|
|
27
39
|
declare const LDFileUpload: {
|
|
28
|
-
({ recommendedSize, isExistDescription, fileList, ...props }: LDFileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
({ recommendedSize, isExistDescription, isExistUploadedAt, fileList, ...props }: LDFileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
29
41
|
Root: ({ className, classNames, children, ...props }: LDFileUploadRootProps) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
-
Input: ({ type, label, className, ...props }: LDFileUploadInputProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
31
|
-
CardList: ({ type, className, }: LDFileUploadCardListProps) => import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
Input: ({ type, label, icon, options, className, ...props }: LDFileUploadInputProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
43
|
+
CardList: ({ type, isExistUploadedAt, className, }: LDFileUploadCardListProps) => import("react/jsx-runtime").JSX.Element;
|
|
44
|
+
LinkList: ({ placeholder, className, }: LDFileUploadLinkListProps) => import("react/jsx-runtime").JSX.Element;
|
|
32
45
|
Description: ({ className, ...props }: LDFileUploadDescriptionProps) => import("react/jsx-runtime").JSX.Element;
|
|
33
46
|
};
|
|
34
47
|
export default LDFileUpload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LDFileUpload.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/LDFileUpload.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"LDFileUpload.d.ts","sourceRoot":"","sources":["../../../src/components/LDFileUpload/LDFileUpload.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAEf,OAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAE9E,OAAuB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EAEvB,MAAM,0CAA0C,CAAC;AAqFlD,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,UAAU,CAAC,EAAE,sBAAsB,CAAC;CACrC;AAuBD,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAClD,aAAa,CAAC,QAAQ,CAAC,EACvB,MAAM,CACP;IACC,IAAI,CAAC,EAAE,SAAS,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mDAAmD;IACnD,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AA6FD,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAC3B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA6HD,MAAM,WAAW,yBAAyB;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwDD,MAAM,MAAM,4BAA4B,GAAG,cAAc,CAAC,oBAAoB,CAAC,CAAC;AA6BhF,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAC7C,qBAAqB,EACrB,UAAU,GAAG,YAAY,GAAG,cAAc,CAC3C;IACC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,4BAA4B;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,kCAAkC;AAClC,QAAA,MAAM,YAAY;qFAMf,iBAAiB;0DAhWjB,qBAAqB;iEAmCrB,sBAAsB;wDA+FtB,yBAAyB;4CA+HzB,yBAAyB;2CAqDkB,4BAA4B;CAwDzE,CAAC;AAQF,eAAe,YAAY,CAAC"}
|