teachable-design-system 0.2.1 → 0.3.2

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.
Files changed (50) hide show
  1. package/dist/index.cjs.js +875 -64
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +72 -2
  4. package/dist/index.esm.js +877 -67
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/types/components/Dropdown/Dropdown.d.ts +1 -1
  7. package/dist/types/components/Dropdown/Dropdown.d.ts.map +1 -1
  8. package/dist/types/components/Dropdown/style.d.ts +1 -0
  9. package/dist/types/components/Dropdown/style.d.ts.map +1 -1
  10. package/dist/types/components/Table/Table.d.ts +3 -0
  11. package/dist/types/components/Table/Table.d.ts.map +1 -0
  12. package/dist/types/components/Table/index.d.ts +3 -0
  13. package/dist/types/components/Table/index.d.ts.map +1 -0
  14. package/dist/types/components/Table/style.d.ts +98 -0
  15. package/dist/types/components/Table/style.d.ts.map +1 -0
  16. package/dist/types/components/Table/table-body.d.ts +3 -0
  17. package/dist/types/components/Table/table-body.d.ts.map +1 -0
  18. package/dist/types/components/Table/table-cell.d.ts +3 -0
  19. package/dist/types/components/Table/table-cell.d.ts.map +1 -0
  20. package/dist/types/components/Table/table-header.d.ts +4 -0
  21. package/dist/types/components/Table/table-header.d.ts.map +1 -0
  22. package/dist/types/components/index.d.ts +1 -0
  23. package/dist/types/components/index.d.ts.map +1 -1
  24. package/dist/types/types/Dropdown.types.d.ts +1 -0
  25. package/dist/types/types/Dropdown.types.d.ts.map +1 -1
  26. package/dist/types/types/index.d.ts +6 -0
  27. package/dist/types/types/index.d.ts.map +1 -1
  28. package/dist/types/types/table.d.ts +141 -0
  29. package/dist/types/types/table.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/src/components/Dropdown/Dropdown.stories.tsx +4 -0
  32. package/src/components/Dropdown/Dropdown.tsx +2 -1
  33. package/src/components/Dropdown/style.ts +2 -1
  34. package/src/components/Sidebar/style.ts +4 -4
  35. package/src/components/Table/Table.stories.tsx +179 -0
  36. package/src/components/Table/Table.tsx +510 -0
  37. package/src/components/Table/index.ts +2 -0
  38. package/src/components/Table/style.ts +345 -0
  39. package/src/components/Table/table-body.tsx +112 -0
  40. package/src/components/Table/table-cell.tsx +153 -0
  41. package/src/components/Table/table-header.tsx +52 -0
  42. package/src/components/index.ts +5 -4
  43. package/src/types/Dropdown.types.ts +1 -0
  44. package/src/types/index.ts +6 -0
  45. package/src/types/table.ts +150 -0
  46. package/dist/assets/icons/arrow-down.png +0 -0
  47. package/dist/assets/icons/checked.png +0 -0
  48. package/dist/assets/icons/icon_size.png +0 -0
  49. package/dist/assets/images/.gitkeep +0 -0
  50. package/dist/assets/index.ts +0 -1
package/dist/index.esm.js CHANGED
@@ -1,13 +1,13 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import styled from '@emotion/styled';
3
3
  import { css } from '@emotion/react';
4
- import React, { useState } from 'react';
4
+ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
5
5
  import element from './assets/icons/checked.png';
6
6
  import arrowDownIcon from './assets/icons/arrow-down.png';
7
- import { Eye, EyeOff } from 'lucide-react';
7
+ import { Eye, EyeOff, ChevronUp, ChevronDown, Copy, ClipboardPaste, Trash2, XCircle } from 'lucide-react';
8
8
  import icon from './assets/icons/icon_size.png';
9
9
 
10
- const colors = {
10
+ const colors$1 = {
11
11
  //mode
12
12
  background: {
13
13
  'white': '#FFFFFF',
@@ -742,26 +742,26 @@ const StyledButton = styled.button `
742
742
  `}
743
743
 
744
744
  ${props => props.buttonType === 'primary' && css `
745
- background-color: ${colors.button["primary-fill"]};
746
- color: ${colors.text["inverse-static"]};
745
+ background-color: ${colors$1.button["primary-fill"]};
746
+ color: ${colors$1.text["inverse-static"]};
747
747
  border: none;
748
748
 
749
749
  &:hover:not(:disabled) {
750
- background-color: ${colors.button["primary-fill-hover"]};
750
+ background-color: ${colors$1.button["primary-fill-hover"]};
751
751
  }
752
752
 
753
753
  &:active:not(:disabled) {
754
- background-color: ${colors.button["tertiary-fill"]};
754
+ background-color: ${colors$1.button["tertiary-fill"]};
755
755
  }
756
756
  `}
757
757
 
758
758
  ${props => props.buttonType === 'secondary' && css `
759
- background-color: ${colors.button['secondary-fill']};
760
- color: ${colors.text['primary']};
761
- border: 1px solid ${colors.button["secondary-border"]};
759
+ background-color: ${colors$1.button['secondary-fill']};
760
+ color: ${colors$1.text['primary']};
761
+ border: 1px solid ${colors$1.button["secondary-border"]};
762
762
 
763
763
  &:hover:not(:disabled) {
764
- background-color: ${colors.button['secondary-fill-hover']};
764
+ background-color: ${colors$1.button['secondary-fill-hover']};
765
765
  }
766
766
 
767
767
  &:active:not(:disabled) {
@@ -771,8 +771,8 @@ const StyledButton = styled.button `
771
771
 
772
772
  ${props => props.buttonType === 'tertiary' && css `
773
773
  background-color: transparent;
774
- color: ${colors.text["basic"]};
775
- border: 1px solid ${colors.button["tertiary-border"]};
774
+ color: ${colors$1.text["basic"]};
775
+ border: 1px solid ${colors$1.button["tertiary-border"]};
776
776
 
777
777
  &:hover:not(:disabled) {
778
778
  background-color: #f0f7ff;
@@ -810,30 +810,30 @@ const Wrapper$1 = styled.div `
810
810
 
811
811
  ${state === "disabled" &&
812
812
  css `
813
- background-color: ${colors.surface.disabled};
814
- border: 1px solid ${colors.border.disabled};
813
+ background-color: ${colors$1.surface.disabled};
814
+ border: 1px solid ${colors$1.border.disabled};
815
815
  `}
816
816
 
817
817
  ${state === "default" &&
818
818
  select === "off" &&
819
819
  css `
820
- background-color: ${colors.surface.white};
821
- border: 1px solid ${colors.border["gray-dark"]};
820
+ background-color: ${colors$1.surface.white};
821
+ border: 1px solid ${colors$1.border["gray-dark"]};
822
822
 
823
823
  &:hover {
824
- border-color: ${colors.border.primary};
824
+ border-color: ${colors$1.border.primary};
825
825
  }
826
826
  `}
827
827
 
828
828
  ${state === "default" &&
829
829
  (select === "on" || select === "indeterminate") &&
830
830
  css `
831
- background-color: ${colors.element.primary};
832
- border: 1px solid ${colors.element.primary};
831
+ background-color: ${colors$1.element.primary};
832
+ border: 1px solid ${colors$1.element.primary};
833
833
 
834
834
  &:hover {
835
- background-color: ${colors.light.primary["60"]};
836
- border-color: ${colors.light.primary["60"]};
835
+ background-color: ${colors$1.light.primary["60"]};
836
+ border-color: ${colors$1.light.primary["60"]};
837
837
  }
838
838
  `}
839
839
  `;
@@ -848,8 +848,8 @@ const DashIcon = styled.div `
848
848
  width: 60%;
849
849
  height: 2.5px;
850
850
  background-color: ${({ state }) => state === "disabled"
851
- ? colors.element["disabled-dark"]
852
- : colors.element.inverse};
851
+ ? colors$1.element["disabled-dark"]
852
+ : colors$1.element.inverse};
853
853
  border-radius: 2px;
854
854
  `;
855
855
 
@@ -956,15 +956,15 @@ const getOptionSize = (size) => {
956
956
  };
957
957
  const StyledDropDown = styled.button `
958
958
  font-family: ${typography.fontFamily.primary};
959
- background: ${colors.input.surface};
959
+ background: ${colors$1.input.surface};
960
960
  border-radius: 8px;
961
961
  display: flex;
962
962
  flex-direction: column;
963
963
  align-items: flex-start;
964
964
  gap: 8px;
965
- width: 320px;
965
+ width: ${({ width }) => width || '320px'};
966
966
 
967
- border: ${({ isOpen }) => isOpen ? `2px solid ${colors.input['border-active']}` : `1px solid ${colors.input.border}`};
967
+ border: ${({ isOpen }) => isOpen ? `2px solid ${colors$1.input['border-active']}` : `1px solid ${colors$1.input.border}`};
968
968
  ${props => getButtonSize(props.size)}
969
969
 
970
970
  `;
@@ -984,17 +984,17 @@ const StyledText = styled.p `
984
984
  flex: 1 0 0;
985
985
  text-align: left;
986
986
  ${props => getFontSize(props.size)}
987
- color: ${({ isOpen }) => isOpen ? colors.text.static : colors.text.disabled};
987
+ color: ${({ isOpen }) => isOpen ? colors$1.text.static : colors$1.text.disabled};
988
988
  `;
989
989
  const StyledLabel = styled.p `
990
990
  font-family: ${typography.fontFamily.primary};
991
- color: ${colors.text.static};
991
+ color: ${colors$1.text.static};
992
992
  `;
993
993
  const StyledIcon$1 = styled.div `
994
994
  ${props => getIconSize(props.size)}
995
995
  `;
996
996
  const StyledOptions = styled.div `
997
- border: 1px solid ${colors.border["gray-light"]};
997
+ border: 1px solid ${colors$1.border["gray-light"]};
998
998
  border-radius: 8px;
999
999
 
1000
1000
  display: flex;
@@ -1019,7 +1019,7 @@ const StyledOption = styled.div `
1019
1019
 
1020
1020
  border-radius: 8px;
1021
1021
 
1022
- color: ${colors.text.subtle};
1022
+ color: ${colors$1.text.subtle};
1023
1023
  position: relative;
1024
1024
 
1025
1025
  ${props => getFontSize(props.size)}
@@ -1027,8 +1027,8 @@ const StyledOption = styled.div `
1027
1027
 
1028
1028
 
1029
1029
  ${({ isSelected }) => isSelected && css `
1030
- background: ${colors.action["secondary-selected"]};
1031
- color: ${colors.text.secondary};
1030
+ background: ${colors$1.action["secondary-selected"]};
1031
+ color: ${colors$1.text.secondary};
1032
1032
 
1033
1033
  padding-left: 36px;
1034
1034
 
@@ -1047,14 +1047,14 @@ const StyledOption = styled.div `
1047
1047
  `}
1048
1048
 
1049
1049
  &:hover {
1050
- background: ${colors.action["secondary-hover"]};
1050
+ background: ${colors$1.action["secondary-hover"]};
1051
1051
  }
1052
1052
  &:active {
1053
- background: ${colors.action["secondary-pressed"]};
1053
+ background: ${colors$1.action["secondary-pressed"]};
1054
1054
  }
1055
1055
  `;
1056
1056
 
1057
- function Dropdown({ size, options, onSelect, label, placeholder }) {
1057
+ function Dropdown({ size, options, onSelect, label, placeholder, width }) {
1058
1058
  const [open, setOpen] = React.useState(false);
1059
1059
  const [selected, setSelected] = React.useState(null);
1060
1060
  const ref = React.useRef(null);
@@ -1072,7 +1072,7 @@ function Dropdown({ size, options, onSelect, label, placeholder }) {
1072
1072
  onSelect?.(option);
1073
1073
  setOpen(false);
1074
1074
  };
1075
- return (jsxs("div", { style: { position: 'relative' }, children: [jsx(StyledLabel, { children: label }), jsx(StyledDropDown, { onClick: () => setOpen((prev) => !prev), size: size, isOpen: open, children: jsxs(StyledBox, { children: [jsx(StyledText, { size: size, isOpen: open, children: selected ?? placeholder }), jsx(StyledIcon$1, { size: size, children: jsx("img", { src: arrowDownIcon, alt: "dropdown icon", style: { width: '100%', height: '100%' } }) })] }) }), open && (jsx(StyledOptions, { ref: ref, size: size, children: options?.map((option) => (jsx(StyledOption, { onClick: () => handleSelect(option), size: size, isSelected: option === selected, children: option }, option))) }))] }));
1075
+ return (jsxs("div", { style: { position: 'relative' }, children: [jsx(StyledLabel, { children: label }), jsx(StyledDropDown, { onClick: () => setOpen((prev) => !prev), size: size, width: width, isOpen: open, children: jsxs(StyledBox, { children: [jsx(StyledText, { size: size, isOpen: open, children: selected ?? placeholder }), jsx(StyledIcon$1, { size: size, children: jsx("img", { src: arrowDownIcon, alt: "dropdown icon", style: { width: '100%', height: '100%' } }) })] }) }), open && (jsx(StyledOptions, { ref: ref, size: size, children: options?.map((option) => (jsx(StyledOption, { onClick: () => handleSelect(option), size: size, isSelected: option === selected, children: option }, option))) }))] }));
1076
1076
  }
1077
1077
 
1078
1078
  const getInputSizeStyle = (size) => {
@@ -1120,7 +1120,7 @@ const Label = styled.label `
1120
1120
  line-height: ${typography.label.small.lineHeight};
1121
1121
  font-weight: ${typography.label.small.fontWeight};
1122
1122
  font-family: ${typography.fontFamily.primary};
1123
- color: ${colors.text.subtle};
1123
+ color: ${colors$1.text.subtle};
1124
1124
  `;
1125
1125
  const InputContainer = styled.div `
1126
1126
  position: relative;
@@ -1132,13 +1132,13 @@ const StyledInput = styled.input `
1132
1132
  width: ${(props) => props.width || "306px"};
1133
1133
  padding: ${(props) => (props.isPassword ? "0px 48px 0px 16px" : "0px 16px")};
1134
1134
  font-family: ${typography.fontFamily.primary};
1135
- border: 1px solid ${colors.input.border};
1135
+ border: 1px solid ${colors$1.input.border};
1136
1136
  border-radius: 4px;
1137
1137
  outline: none;
1138
1138
  transition: all 0.2s ease;
1139
- background-color: ${(props) => props.disabled ? colors.input["surface-disabled"] : colors.input.surface};
1139
+ background-color: ${(props) => props.disabled ? colors$1.input["surface-disabled"] : colors$1.input.surface};
1140
1140
  cursor: ${(props) => (props.disabled ? "not-allowed" : "text")};
1141
- color: ${colors.text.basic};
1141
+ color: ${colors$1.text.basic};
1142
1142
  box-sizing: border-box;
1143
1143
 
1144
1144
  ${(props) => getInputSizeStyle(props.inputSize)}
@@ -1149,17 +1149,17 @@ const StyledInput = styled.input `
1149
1149
  `}
1150
1150
 
1151
1151
  &:focus {
1152
- border: 1px solid ${colors.input["border-active"]};
1153
- box-shadow: 0 0 0 3px ${colors.light.primary["5"]};
1152
+ border: 1px solid ${colors$1.input["border-active"]};
1153
+ box-shadow: 0 0 0 3px ${colors$1.light.primary["5"]};
1154
1154
  }
1155
1155
 
1156
1156
  &:disabled {
1157
- border: 1px solid ${colors.input["border-disabled"]};
1158
- color: ${colors.text.disabled};
1157
+ border: 1px solid ${colors$1.input["border-disabled"]};
1158
+ color: ${colors$1.text.disabled};
1159
1159
  }
1160
1160
 
1161
1161
  &::placeholder {
1162
- color: ${colors.text.disabled};
1162
+ color: ${colors$1.text.disabled};
1163
1163
  }
1164
1164
  `;
1165
1165
  const IconButton = styled.button `
@@ -1178,7 +1178,7 @@ const IconButton = styled.button `
1178
1178
  transition: opacity 0.2s ease;
1179
1179
 
1180
1180
  svg {
1181
- color: ${(props) => props.disabled ? colors.icon.disabled : colors.icon.gray};
1181
+ color: ${(props) => props.disabled ? colors$1.icon.disabled : colors$1.icon.gray};
1182
1182
  }
1183
1183
  `;
1184
1184
 
@@ -1196,14 +1196,14 @@ const StyledIcon = styled.img `
1196
1196
  `;
1197
1197
  const StyledSidebar = styled.div `
1198
1198
  display: flex;
1199
- width: 240px;
1200
- height: 100vh;
1199
+ width: 208px;
1200
+ height: 621px;
1201
1201
  padding: 16px;
1202
1202
  flex-direction: column;
1203
1203
  align-items: flex-start;
1204
1204
  gap: 16px;
1205
1205
 
1206
- background: ${colors.background.white};
1206
+ background: ${colors$1.background.white};
1207
1207
  `;
1208
1208
  const StyledDescription = styled.div `
1209
1209
  width: 100%;
@@ -1212,7 +1212,7 @@ const StyledDescription = styled.div `
1212
1212
  align-items: flex-start;
1213
1213
  gap: 18px;
1214
1214
  flex: 1 0 0;
1215
- max-height: calc(100vh - 40px);
1215
+ max-height: calc(100% - 40px);
1216
1216
  overflow-y: auto;
1217
1217
  overflow-x: hidden;
1218
1218
 
@@ -1221,21 +1221,21 @@ const StyledDescription = styled.div `
1221
1221
  }
1222
1222
 
1223
1223
  ::-webkit-scrollbar-track {
1224
- background: ${colors.surface["gray-subtler"]};
1224
+ background: ${colors$1.surface["gray-subtler"]};
1225
1225
  border-radius: 4px;
1226
1226
  }
1227
1227
 
1228
1228
  ::-webkit-scrollbar-thumb {
1229
- background-color: ${colors.text.subtle};
1229
+ background-color: ${colors$1.text.subtle};
1230
1230
  border-radius: 4px;
1231
1231
  }
1232
1232
 
1233
1233
  ::-webkit-scrollbar-thumb:hover {
1234
- background-color: ${colors.action["secondary-active"]};
1234
+ background-color: ${colors$1.action["secondary-active"]};
1235
1235
  }
1236
1236
 
1237
1237
  ::-webkit-scrollbar-thumb:active {
1238
- background-color: ${colors.action["primary-active"]};
1238
+ background-color: ${colors$1.action["primary-active"]};
1239
1239
  }
1240
1240
  `;
1241
1241
  const StyledButtonArea = styled.div `
@@ -1266,15 +1266,15 @@ const StyledOpenContents = styled.div `
1266
1266
  align-items: flex-start;
1267
1267
  gap: 8px;
1268
1268
  align-self: stretch;
1269
- width: 186px;
1269
+ width: 154px;
1270
1270
 
1271
1271
  border-radius: 12px;
1272
- background: ${colors.surface["gray-subtler"]};
1272
+ background: ${colors$1.surface["gray-subtler"]};
1273
1273
  `;
1274
1274
  const StyledOpenContentsText = styled.p `
1275
1275
  align-self: stretch;
1276
1276
 
1277
- color: ${colors.text.subtle};
1277
+ color: ${colors$1.text.subtle};
1278
1278
 
1279
1279
  font-family: ${typography.fontFamily.primary};
1280
1280
  font-size: 13px;
@@ -1311,7 +1311,7 @@ const Wrapper = styled.div `
1311
1311
  width: 160px;
1312
1312
  height: 100vh;
1313
1313
  padding: 16px;
1314
- background-color: ${colors.surface.white};
1314
+ background-color: ${colors$1.surface.white};
1315
1315
  gap: 16px;
1316
1316
  `;
1317
1317
  const Title = styled.span `
@@ -1319,7 +1319,7 @@ const Title = styled.span `
1319
1319
  margin: 0;
1320
1320
  ${typography.heading.xxsmall}
1321
1321
  font-family:${typography.fontFamily.primary};
1322
- color: ${colors.text.bolder};
1322
+ color: ${colors$1.text.bolder};
1323
1323
  `;
1324
1324
  const Tablist = styled.div `
1325
1325
  display: flex;
@@ -1330,19 +1330,19 @@ const TabItem = styled.div `
1330
1330
  padding: 8px 8px;
1331
1331
  font-family: ${typography.fontFamily.primary};
1332
1332
  ${({ isSelected }) => isSelected ? typography.body.xsmallBold : typography.body.xsmall}
1333
- color: ${({ isSelected }) => isSelected ? colors.text.secondary : colors.text.subtle};
1334
- background-color: ${({ isSelected }) => isSelected ? colors.action["secondary-selected"] : "transparent"};
1333
+ color: ${({ isSelected }) => isSelected ? colors$1.text.secondary : colors$1.text.subtle};
1334
+ background-color: ${({ isSelected }) => isSelected ? colors$1.action["secondary-selected"] : "transparent"};
1335
1335
  border-radius: 4px;
1336
1336
  cursor: pointer;
1337
1337
 
1338
1338
  &:hover {
1339
1339
  background-color: ${({ isSelected }) => isSelected
1340
- ? colors.action["secondary-selected"]
1341
- : colors.action["secondary-hover"]};
1340
+ ? colors$1.action["secondary-selected"]
1341
+ : colors$1.action["secondary-hover"]};
1342
1342
  }
1343
1343
 
1344
1344
  &:active {
1345
- background-color: ${colors.action["secondary-pressed"]};
1345
+ background-color: ${colors$1.action["secondary-pressed"]};
1346
1346
  }
1347
1347
  `;
1348
1348
 
@@ -1357,5 +1357,815 @@ const TabBar = ({ title, items, defaultSelectedId, onChange }) => {
1357
1357
  return (jsxs(Wrapper, { children: [title && jsx(Title, { children: title }), jsx(Tablist, { children: items.map((item) => (jsx(TabItem, { isSelected: selectedId === item.id, onClick: () => handleTabClick(item.id), children: item.label }, item.id))) })] }));
1358
1358
  };
1359
1359
 
1360
- export { Button, CheckBox, Dropdown, Input, Sidebar, TabBar, colors, typography };
1360
+ // ============================================
1361
+ // 디자인 토큰
1362
+ // ============================================
1363
+ const colors = {
1364
+ header: colors$1.surface['secondary-subtler'],
1365
+ headerHover: colors$1.action['secondary-pressed'],
1366
+ body: colors$1.surface.white,
1367
+ bodyHover: colors$1.surface['gray-subtler'],
1368
+ border: colors$1.border['gray-light'],
1369
+ borderLight: colors$1.border['secondary-light'],
1370
+ text: colors$1.text.bolder,
1371
+ textSecondary: colors$1.text.subtle,
1372
+ scrollThumb: colors$1.surface['gray-subtle'],
1373
+ scrollThumbBorder: colors$1.border.gray,
1374
+ selected: colors$1.surface['information-subtler'],
1375
+ selectedBorder: colors$1.border.primary,
1376
+ disabledText: colors$1.text.disabled,
1377
+ };
1378
+ const spacing = {
1379
+ cellPadding: '4px 16px',
1380
+ headerPadding: '4px 16px',
1381
+ };
1382
+ // ============================================
1383
+ // 레이아웃 컴포넌트
1384
+ // ============================================
1385
+ const TableOuterWrapper = styled.div `
1386
+ position: relative;
1387
+ display: inline-block;
1388
+ outline: none;
1389
+
1390
+ &:focus {
1391
+ outline: none;
1392
+ }
1393
+ `;
1394
+ const TableWrapper = styled.div `
1395
+ display: flex;
1396
+ flex-direction: column;
1397
+ width: 100%;
1398
+ overflow: hidden;
1399
+ `;
1400
+ const TableContainer = styled.div `
1401
+ width: 100%;
1402
+ overflow: auto;
1403
+ position: relative;
1404
+ ${({ maxHeight }) => maxHeight && `max-height: ${maxHeight};`}
1405
+
1406
+ /* 스크롤바 스타일 */
1407
+ &::-webkit-scrollbar {
1408
+ width: 20px;
1409
+ height: 20px;
1410
+ }
1411
+
1412
+ &::-webkit-scrollbar-track {
1413
+ background: ${colors.body};
1414
+ border: 1px solid ${colors.border};
1415
+ margin-top: 20px; /* 상단 화살표 버튼 높이만큼 여백 */
1416
+ }
1417
+
1418
+ &::-webkit-scrollbar-thumb {
1419
+ background: ${colors.scrollThumb};
1420
+ border: 1px solid ${colors.scrollThumbBorder};
1421
+
1422
+ &:hover {
1423
+ background: ${colors.headerHover};
1424
+ }
1425
+ }
1426
+
1427
+ &::-webkit-scrollbar-button:vertical:start:decrement,
1428
+ &::-webkit-scrollbar-button:vertical:end:increment {
1429
+ display: block;
1430
+ height: 20px;
1431
+ background: ${colors.header};
1432
+ border: 1px solid ${colors.borderLight};
1433
+ }
1434
+
1435
+ &::-webkit-scrollbar-button:vertical:start:decrement:hover,
1436
+ &::-webkit-scrollbar-button:vertical:end:increment:hover {
1437
+ background: ${colors.headerHover};
1438
+ }
1439
+ `;
1440
+ // ============================================
1441
+ // 테이블 기본 컴포넌트
1442
+ // ============================================
1443
+ const StyledTable = styled.table `
1444
+ width: 100%;
1445
+ border-collapse: separate;
1446
+ border-spacing: 0;
1447
+ table-layout: auto;
1448
+ font-family: ${typography.fontFamily.primary};
1449
+ `;
1450
+ const TableHead = styled.thead `
1451
+ position: sticky;
1452
+ top: 0;
1453
+ z-index: 10;
1454
+ background: ${colors.header};
1455
+ `;
1456
+ const TableBody$1 = styled.tbody ``;
1457
+ const TableRow = styled.tr `
1458
+ &:nth-of-type(even) {
1459
+ ${({ striped }) => striped && `background-color: ${colors.bodyHover};`}
1460
+ }
1461
+
1462
+ &:hover {
1463
+ background-color: ${colors.bodyHover};
1464
+ }
1465
+ `;
1466
+ // ============================================
1467
+ // 셀 컴포넌트
1468
+ // ============================================
1469
+ const baseCellStyle = `
1470
+ box-sizing: border-box;
1471
+ vertical-align: middle;
1472
+ line-height: 1.5;
1473
+ `;
1474
+ const TableHeaderCell = styled.th `
1475
+ ${baseCellStyle}
1476
+ min-width: ${({ width }) => (width ? '0' : '80px')};
1477
+ background: ${colors.header};
1478
+ border: 1px solid ${colors.borderLight};
1479
+ border-left: none;
1480
+ padding: ${spacing.headerPadding};
1481
+ text-align: left;
1482
+ font-weight: 700;
1483
+ font-size: 15px;
1484
+ color: ${colors.text};
1485
+ height: 30px;
1486
+ white-space: nowrap;
1487
+ position: relative;
1488
+ ${({ width }) => width && `width: ${width};`}
1489
+
1490
+ &:first-of-type {
1491
+ border-left: 1px solid ${colors.borderLight};
1492
+ }
1493
+
1494
+ ${({ sortable }) => sortable &&
1495
+ `
1496
+ cursor: pointer;
1497
+ user-select: none;
1498
+
1499
+ &:hover {
1500
+ background: ${colors.headerHover};
1501
+ }
1502
+ `}
1503
+ `;
1504
+ const TableDataCell = styled.td `
1505
+ ${baseCellStyle}
1506
+ min-width: ${({ width }) => (width ? '0' : '80px')};
1507
+ background: ${({ isHeaderColumn, isSelected, $rowSelected }) => isSelected ? colors.selected : (isHeaderColumn ? colors.header : ($rowSelected ? 'inherit' : colors.body))};
1508
+ border-right: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1509
+ border-bottom: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1510
+ border-left: none;
1511
+ border-top: none;
1512
+ padding: ${({ isHeaderColumn }) => (isHeaderColumn ? spacing.headerPadding : spacing.cellPadding)};
1513
+ font-weight: ${({ isHeaderColumn }) => (isHeaderColumn ? 700 : 400)};
1514
+ font-size: ${({ isHeaderColumn }) => (isHeaderColumn ? '15px' : '13px')};
1515
+ color: ${({ isHeaderColumn }) => (isHeaderColumn ? colors.text : colors.textSecondary)};
1516
+ height: ${({ height }) => height ?? '30px'};
1517
+ position: relative;
1518
+ user-select: none;
1519
+
1520
+ &:first-of-type {
1521
+ border-left: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1522
+ }
1523
+
1524
+ ${({ isSelected, $edgeTop, $edgeBottom, $edgeLeft, $edgeRight }) => isSelected &&
1525
+ `
1526
+ z-index: 1;
1527
+ box-shadow:
1528
+ ${$edgeTop ? `inset 0 2px 0 0 ${colors.selectedBorder}` : `inset 0 0.5px 0 0 ${colors.selectedBorder}50`}${$edgeBottom ? `, inset 0 -2px 0 0 ${colors.selectedBorder}` : `, inset 0 -0.5px 0 0 ${colors.selectedBorder}50`}${$edgeLeft ? `, inset 2px 0 0 0 ${colors.selectedBorder}` : `, inset 0.5px 0 0 0 ${colors.selectedBorder}50`}${$edgeRight ? `, inset -2px 0 0 0 ${colors.selectedBorder}` : `, inset -0.5px 0 0 0 ${colors.selectedBorder}50`};
1529
+ `}
1530
+
1531
+ ${({ editable, isSelected, $rowSelected }) => editable && !isSelected && !$rowSelected &&
1532
+ `
1533
+ cursor: cell;
1534
+ &:hover {
1535
+ background-color: ${colors.bodyHover};
1536
+ }
1537
+ `}
1538
+
1539
+ ${({ isSelected }) => isSelected &&
1540
+ `
1541
+ &:hover {
1542
+ background-color: ${colors.selected};
1543
+ }
1544
+ `}
1545
+ `;
1546
+ // ============================================
1547
+ // 정렬 및 입력 컴포넌트
1548
+ // ============================================
1549
+ const SortIcon = styled.span `
1550
+ display: inline-flex;
1551
+ flex-direction: column;
1552
+ margin-left: 4px;
1553
+ opacity: ${({ active }) => (active ? 1 : 0.3)};
1554
+
1555
+ svg {
1556
+ width: 12px;
1557
+ height: 12px;
1558
+ }
1559
+ `;
1560
+ const EditableInput = styled.input `
1561
+ width: 100%;
1562
+ border: none;
1563
+ outline: none;
1564
+ background: transparent;
1565
+ font: inherit;
1566
+ color: inherit;
1567
+ margin: -12px -16px;
1568
+ padding: ${spacing.cellPadding};
1569
+
1570
+ &:focus {
1571
+ outline: none;
1572
+ }
1573
+ `;
1574
+ // ============================================
1575
+ // 스크롤 컨트롤
1576
+ // ============================================
1577
+ const ScrollContainer = styled.div `
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ position: absolute;
1581
+ right: 0;
1582
+ top: 0;
1583
+ `;
1584
+ const ScrollButton = styled.button `
1585
+ width: 20px;
1586
+ height: 20px;
1587
+ padding: 0;
1588
+ background: ${colors.header};
1589
+ border: 1px solid ${colors.borderLight};
1590
+ cursor: pointer;
1591
+ display: flex;
1592
+ align-items: center;
1593
+ justify-content: center;
1594
+ transition: background-color 0.2s;
1595
+
1596
+ &:hover {
1597
+ background: ${colors.headerHover};
1598
+ }
1599
+
1600
+ &:disabled {
1601
+ opacity: 0.5;
1602
+ cursor: not-allowed;
1603
+ }
1604
+
1605
+ svg {
1606
+ width: 14px;
1607
+ height: 14px;
1608
+ color: ${colors.text};
1609
+ }
1610
+ `;
1611
+ // ============================================
1612
+ // 컨텍스트 메뉴
1613
+ // ============================================
1614
+ const ContextMenuOverlay = styled.div `
1615
+ position: fixed;
1616
+ top: 0;
1617
+ left: 0;
1618
+ right: 0;
1619
+ bottom: 0;
1620
+ z-index: 999;
1621
+ `;
1622
+ const ContextMenu = styled.div `
1623
+ position: fixed;
1624
+ top: ${({ y }) => y}px;
1625
+ left: ${({ x }) => x}px;
1626
+ z-index: 1000;
1627
+ min-width: 160px;
1628
+ background: ${colors.body};
1629
+ border: 1px solid ${colors.border};
1630
+ border-radius: 6px;
1631
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1632
+ padding: 4px 0;
1633
+ font-family: ${typography.fontFamily.primary};
1634
+ `;
1635
+ const ContextMenuItem = styled.button `
1636
+ width: 100%;
1637
+ padding: 8px 12px;
1638
+ border: none;
1639
+ background: transparent;
1640
+ text-align: left;
1641
+ font-size: 13px;
1642
+ color: ${({ disabled }) => (disabled ? colors.disabledText : colors.text)};
1643
+ cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
1644
+ display: flex;
1645
+ align-items: center;
1646
+ gap: 8px;
1647
+
1648
+ &:hover {
1649
+ background: ${({ disabled }) => (disabled ? 'transparent' : colors.bodyHover)};
1650
+ }
1651
+
1652
+ span.shortcut {
1653
+ margin-left: auto;
1654
+ font-size: 11px;
1655
+ color: ${colors.disabledText};
1656
+ }
1657
+ `;
1658
+ const ContextMenuDivider = styled.div `
1659
+ height: 1px;
1660
+ background: ${colors.borderLight};
1661
+ margin: 4px 0;
1662
+ `;
1663
+
1664
+ /** 정렬 아이콘 컴포넌트 */
1665
+ function SortIndicator({ isActive, direction }) {
1666
+ if (!isActive || !direction) {
1667
+ return jsx(ChevronUp, { style: { opacity: 0.3 } });
1668
+ }
1669
+ return direction === 'asc' ? jsx(ChevronUp, {}) : jsx(ChevronDown, {});
1670
+ }
1671
+ /** 테이블 헤더 */
1672
+ function TableHeader({ columns, sortColumn, sortDirection, onSort, }) {
1673
+ const handleClick = (key, sortable) => {
1674
+ if (sortable)
1675
+ onSort?.(key);
1676
+ };
1677
+ return (jsx(TableHead, { children: jsx(TableRow, { children: columns.map(({ key, header, width, sortable, rowSpan, colSpan }) => {
1678
+ const isActive = sortColumn === key;
1679
+ return (jsxs(TableHeaderCell, { width: width, sortable: sortable, rowSpan: rowSpan, colSpan: colSpan, onClick: () => handleClick(key, sortable), children: [header, sortable && (jsx(SortIcon, { active: isActive, direction: isActive ? sortDirection : null, children: jsx(SortIndicator, { isActive: isActive, direction: sortDirection }) }))] }, key));
1680
+ }) }) }));
1681
+ }
1682
+
1683
+ const formatValue = (val, dataType) => {
1684
+ if (val == null)
1685
+ return '';
1686
+ switch (dataType) {
1687
+ case 'number':
1688
+ return typeof val === 'number' ? val.toLocaleString() : String(val);
1689
+ case 'date':
1690
+ return val instanceof Date ? val.toLocaleDateString('ko-KR') : String(val);
1691
+ case 'boolean':
1692
+ return val ? '예' : '아니오';
1693
+ default:
1694
+ return String(val);
1695
+ }
1696
+ };
1697
+ const parseValue = (val, dataType) => {
1698
+ switch (dataType) {
1699
+ case 'number': {
1700
+ const num = parseFloat(val.replace(/,/g, ''));
1701
+ return isNaN(num) ? 0 : num;
1702
+ }
1703
+ case 'date':
1704
+ return new Date(val);
1705
+ case 'boolean':
1706
+ return val === '예' || val === 'true' || val === '1';
1707
+ default:
1708
+ return val;
1709
+ }
1710
+ };
1711
+ const getInputType = (dataType) => {
1712
+ if (dataType === 'number')
1713
+ return 'number';
1714
+ if (dataType === 'date')
1715
+ return 'date';
1716
+ return 'text';
1717
+ };
1718
+ function TableCell({ value, editable = false, width, height, rowHeight, dataType = 'text', isHeaderColumn = false, isSelected = false, rowSelected = false, isEditingRequested, startEditingToken, startEditingValue, selectionEdge, rowSpan, colSpan, onEdit, render, onMouseDown, onMouseEnter, onMouseUp, }) {
1719
+ const [isEditing, setIsEditing] = useState(false);
1720
+ const [editValue, setEditValue] = useState('');
1721
+ const lastStartTokenRef = useRef(undefined);
1722
+ const startEditing = useCallback(() => {
1723
+ if (!editable)
1724
+ return;
1725
+ setEditValue(formatValue(value, dataType));
1726
+ setIsEditing(true);
1727
+ }, [editable, value, dataType]);
1728
+ // 부모에서 "편집 시작" 요청이 오면 (예: 선택 셀에서 타이핑) 편집 모드로 진입
1729
+ useEffect(() => {
1730
+ if (!isEditingRequested)
1731
+ return;
1732
+ if (startEditingToken == null)
1733
+ return;
1734
+ if (lastStartTokenRef.current === startEditingToken)
1735
+ return;
1736
+ lastStartTokenRef.current = startEditingToken;
1737
+ if (!editable)
1738
+ return;
1739
+ const nextValue = startEditingValue ?? formatValue(value, dataType);
1740
+ setEditValue(nextValue);
1741
+ setIsEditing(true);
1742
+ }, [isEditingRequested, startEditingToken, startEditingValue, editable, value, dataType]);
1743
+ const save = useCallback(() => {
1744
+ onEdit?.(parseValue(editValue, dataType));
1745
+ setIsEditing(false);
1746
+ }, [editValue, dataType, onEdit]);
1747
+ const cancel = useCallback(() => {
1748
+ setEditValue('');
1749
+ setIsEditing(false);
1750
+ }, []);
1751
+ const handleKeyDown = useCallback((e) => {
1752
+ if (e.key === 'Enter') {
1753
+ e.preventDefault();
1754
+ save();
1755
+ }
1756
+ else if (e.key === 'Escape') {
1757
+ e.preventDefault();
1758
+ cancel();
1759
+ }
1760
+ }, [save, cancel]);
1761
+ const handleMouseDown = useCallback((e) => {
1762
+ if (isEditing)
1763
+ return;
1764
+ e.preventDefault();
1765
+ onMouseDown?.();
1766
+ }, [isEditing, onMouseDown]);
1767
+ const handleMouseEnter = useCallback(() => {
1768
+ if (isEditing)
1769
+ return;
1770
+ onMouseEnter?.();
1771
+ }, [isEditing, onMouseEnter]);
1772
+ const displayValue = render ? render(value) : formatValue(value, dataType);
1773
+ return (jsx(TableDataCell, { editable: editable, width: width, height: height || rowHeight, isHeaderColumn: isHeaderColumn, isSelected: isSelected, "$rowSelected": rowSelected, "$edgeTop": selectionEdge?.top, "$edgeBottom": selectionEdge?.bottom, "$edgeLeft": selectionEdge?.left, "$edgeRight": selectionEdge?.right, rowSpan: rowSpan, colSpan: colSpan, onDoubleClick: startEditing, onMouseDown: handleMouseDown, onMouseEnter: handleMouseEnter, onMouseUp: onMouseUp, children: isEditing ? (jsx(EditableInput, { type: getInputType(dataType), value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: save, onKeyDown: handleKeyDown, autoFocus: true })) : (displayValue) }));
1774
+ }
1775
+
1776
+ const getSelectionInfo = (rowIndex, colIndex, start, end) => {
1777
+ if (!start || !end) {
1778
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
1779
+ }
1780
+ const minRow = Math.min(start.row, end.row);
1781
+ const maxRow = Math.max(start.row, end.row);
1782
+ const minCol = Math.min(start.col, end.col);
1783
+ const maxCol = Math.max(start.col, end.col);
1784
+ const isSelected = rowIndex >= minRow && rowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
1785
+ if (!isSelected) {
1786
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
1787
+ }
1788
+ return {
1789
+ isSelected: true,
1790
+ edge: {
1791
+ top: rowIndex === minRow,
1792
+ bottom: rowIndex === maxRow,
1793
+ left: colIndex === minCol,
1794
+ right: colIndex === maxCol,
1795
+ }
1796
+ };
1797
+ };
1798
+ function TableBody({ columns, data, rowHeight, onCellEdit, selectionStart, selectionEnd, editingCell, editStartValue, editToken, onCellMouseDown, onCellMouseEnter, onCellMouseUp, enableRowSelection, selectedRowIndex, hoveredRowIndex, onRowClick, onRowHover, }) {
1799
+ const getRowBackground = (rowIndex) => {
1800
+ if (!enableRowSelection)
1801
+ return undefined;
1802
+ if (selectedRowIndex === rowIndex)
1803
+ return '#e7f4fe';
1804
+ if (hoveredRowIndex === rowIndex)
1805
+ return '#f4f5f6';
1806
+ return undefined;
1807
+ };
1808
+ return (jsx(TableBody$1, { children: data.map((row, rowIndex) => (jsx(TableRow, { onClick: enableRowSelection ? () => onRowClick?.(rowIndex) : undefined, onMouseEnter: enableRowSelection ? () => onRowHover?.(rowIndex) : undefined, onMouseLeave: enableRowSelection ? () => onRowHover?.(null) : undefined, style: {
1809
+ cursor: enableRowSelection ? 'pointer' : undefined,
1810
+ backgroundColor: getRowBackground(rowIndex),
1811
+ transition: enableRowSelection ? 'background-color 0.15s ease' : undefined,
1812
+ }, children: columns.map((col, colIndex) => {
1813
+ const { isSelected, edge } = getSelectionInfo(rowIndex, colIndex, selectionStart, selectionEnd);
1814
+ const isEditingRequested = !!editingCell && editingCell.row === rowIndex && editingCell.col === colIndex;
1815
+ return (jsx(TableCell, { value: row[col.key], editable: col.editable !== false, width: col.width, height: col.height, rowHeight: rowHeight, dataType: col.dataType, isHeaderColumn: col.isHeaderColumn, isSelected: isSelected, rowSelected: enableRowSelection && (selectedRowIndex === rowIndex || hoveredRowIndex === rowIndex), isEditingRequested: isEditingRequested, startEditingToken: editToken, startEditingValue: isEditingRequested ? editStartValue : null, selectionEdge: isSelected ? edge : undefined, rowSpan: col.rowSpan, colSpan: col.colSpan, onEdit: (value) => onCellEdit?.(rowIndex, col.key, value), render: col.render ? (value) => col.render(value, row, rowIndex) : undefined, onMouseDown: enableRowSelection ? undefined : () => onCellMouseDown?.(rowIndex, colIndex), onMouseEnter: enableRowSelection ? undefined : () => onCellMouseEnter?.(rowIndex, colIndex), onMouseUp: enableRowSelection ? undefined : onCellMouseUp }, `${rowIndex}-${col.key}`));
1816
+ }) }, rowIndex))) }));
1817
+ }
1818
+
1819
+ const compareValues = (a, b, dataType) => {
1820
+ if (a == null)
1821
+ return 1;
1822
+ if (b == null)
1823
+ return -1;
1824
+ switch (dataType) {
1825
+ case 'number':
1826
+ return Number(a) - Number(b);
1827
+ case 'date':
1828
+ return new Date(a).getTime() - new Date(b).getTime();
1829
+ case 'boolean':
1830
+ return (a ? 1 : 0) - (b ? 1 : 0);
1831
+ default:
1832
+ return String(a).localeCompare(String(b), 'ko-KR');
1833
+ }
1834
+ };
1835
+ const getNextSortDirection = (current) => {
1836
+ if (current === 'asc')
1837
+ return 'desc';
1838
+ if (current === 'desc')
1839
+ return null;
1840
+ return 'asc';
1841
+ };
1842
+ const formatCellValue = (value) => {
1843
+ if (value == null)
1844
+ return '';
1845
+ if (value instanceof Date)
1846
+ return value.toLocaleDateString('ko-KR');
1847
+ if (typeof value === 'boolean')
1848
+ return value ? '예' : '아니오';
1849
+ if (typeof value === 'number')
1850
+ return value.toString();
1851
+ return String(value);
1852
+ };
1853
+ function Table({ columns, data, onCellEdit, onSort, onSelectionChange, onPaste, maxHeight, rowHeight, className, enableRowSelection, selectedRowIndex, onRowClick, enableKeyboardNavigation, }) {
1854
+ const outerRef = useRef(null);
1855
+ const containerRef = useRef(null);
1856
+ const [sortColumn, setSortColumn] = useState(null);
1857
+ const [sortDirection, setSortDirection] = useState(null);
1858
+ const [isSelecting, setIsSelecting] = useState(false);
1859
+ const [selectionStart, setSelectionStart] = useState(null);
1860
+ const [selectionEnd, setSelectionEnd] = useState(null);
1861
+ const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0 });
1862
+ const [hoveredRowIndex, setHoveredRowIndex] = useState(null);
1863
+ const [editingCell, setEditingCell] = useState(null);
1864
+ const [editStartValue, setEditStartValue] = useState(null);
1865
+ const [editToken, setEditToken] = useState(0);
1866
+ const sortedData = useMemo(() => {
1867
+ if (!sortColumn || !sortDirection)
1868
+ return data;
1869
+ const column = columns.find((col) => col.key === sortColumn);
1870
+ if (!column)
1871
+ return data;
1872
+ return [...data].sort((a, b) => {
1873
+ const aVal = a[sortColumn];
1874
+ const bVal = b[sortColumn];
1875
+ const result = column.sortFn
1876
+ ? column.sortFn(aVal, bVal)
1877
+ : compareValues(aVal, bVal, column.dataType);
1878
+ return sortDirection === 'asc' ? result : -result;
1879
+ });
1880
+ }, [data, sortColumn, sortDirection, columns]);
1881
+ const handleSort = useCallback((columnKey) => {
1882
+ const isSameColumn = sortColumn === columnKey;
1883
+ const nextDirection = isSameColumn
1884
+ ? getNextSortDirection(sortDirection)
1885
+ : 'asc';
1886
+ setSortColumn(nextDirection ? columnKey : null);
1887
+ setSortDirection(nextDirection);
1888
+ onSort?.(columnKey, nextDirection);
1889
+ }, [sortColumn, sortDirection, onSort]);
1890
+ const scroll = useCallback((delta) => {
1891
+ containerRef.current?.scrollBy({ top: delta, behavior: 'smooth' });
1892
+ }, []);
1893
+ const handleCellMouseDown = useCallback((rowIndex, colIndex) => {
1894
+ setIsSelecting(true);
1895
+ setSelectionStart({ row: rowIndex, col: colIndex });
1896
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1897
+ }, []);
1898
+ const handleCellMouseEnter = useCallback((rowIndex, colIndex) => {
1899
+ if (isSelecting) {
1900
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1901
+ }
1902
+ }, [isSelecting]);
1903
+ const handleCellMouseUp = useCallback(() => {
1904
+ if (isSelecting && selectionStart && selectionEnd) {
1905
+ const cells = [];
1906
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
1907
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
1908
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
1909
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
1910
+ for (let r = minRow; r <= maxRow; r++) {
1911
+ for (let c = minCol; c <= maxCol; c++) {
1912
+ cells.push({ row: r, col: c });
1913
+ }
1914
+ }
1915
+ onSelectionChange?.(cells);
1916
+ }
1917
+ setIsSelecting(false);
1918
+ }, [isSelecting, selectionStart, selectionEnd, onSelectionChange]);
1919
+ const getSelectedData = useCallback(() => {
1920
+ if (!selectionStart || !selectionEnd)
1921
+ return '';
1922
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
1923
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
1924
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
1925
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
1926
+ const rows = [];
1927
+ for (let r = minRow; r <= maxRow; r++) {
1928
+ const cols = [];
1929
+ for (let c = minCol; c <= maxCol; c++) {
1930
+ const colKey = columns[c]?.key;
1931
+ const value = sortedData[r]?.[colKey];
1932
+ cols.push(formatCellValue(value));
1933
+ }
1934
+ rows.push(cols.join('\t'));
1935
+ }
1936
+ return rows.join('\n');
1937
+ }, [selectionStart, selectionEnd, columns, sortedData]);
1938
+ const handleKeyDown = useCallback((e) => {
1939
+ // 셀 편집(input/textarea) 중에는 전역 키 핸들러가 Backspace/Delete 등을 가로채지 않도록 무시
1940
+ const target = e.target;
1941
+ if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
1942
+ return;
1943
+ }
1944
+ // 행 선택 모드에서 키보드 네비게이션
1945
+ if (enableRowSelection && enableKeyboardNavigation && onRowClick) {
1946
+ if (e.key === 'ArrowUp') {
1947
+ e.preventDefault();
1948
+ const currentIndex = selectedRowIndex ?? 0;
1949
+ const newIndex = Math.max(0, currentIndex - 1);
1950
+ onRowClick(newIndex, sortedData[newIndex]);
1951
+ return;
1952
+ }
1953
+ if (e.key === 'ArrowDown') {
1954
+ e.preventDefault();
1955
+ const currentIndex = selectedRowIndex ?? -1;
1956
+ const newIndex = Math.min(sortedData.length - 1, currentIndex + 1);
1957
+ onRowClick(newIndex, sortedData[newIndex]);
1958
+ return;
1959
+ }
1960
+ }
1961
+ if (!selectionStart || !selectionEnd)
1962
+ return;
1963
+ // 셀 선택 상태에서 문자 입력 시 즉시 편집 모드로 전환 (스프레드시트 UX)
1964
+ // - 단일 셀 선택일 때만 동작
1965
+ // - onCellEdit가 있어야 의미 있는 편집이 가능
1966
+ if (onCellEdit &&
1967
+ !e.ctrlKey &&
1968
+ !e.metaKey &&
1969
+ !e.altKey &&
1970
+ e.key.length === 1) {
1971
+ const isSingleCell = selectionStart.row === selectionEnd.row && selectionStart.col === selectionEnd.col;
1972
+ if (!isSingleCell) {
1973
+ setSelectionEnd(selectionStart);
1974
+ }
1975
+ e.preventDefault();
1976
+ setEditingCell({ row: selectionStart.row, col: selectionStart.col });
1977
+ setEditStartValue(e.key);
1978
+ setEditToken((t) => t + 1);
1979
+ setContextMenu({ visible: false, x: 0, y: 0 });
1980
+ return;
1981
+ }
1982
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
1983
+ e.preventDefault();
1984
+ const text = getSelectedData();
1985
+ navigator.clipboard.writeText(text).catch(console.error);
1986
+ }
1987
+ if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
1988
+ e.preventDefault();
1989
+ navigator.clipboard.readText().then((text) => {
1990
+ if (!text || !selectionStart)
1991
+ return;
1992
+ const rows = text
1993
+ .replace(/\r\n/g, '\n')
1994
+ .replace(/\r/g, '\n')
1995
+ .split('\n')
1996
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
1997
+ .map(row => row.split('\t'));
1998
+ const startRow = Math.min(selectionStart.row, selectionEnd?.row ?? selectionStart.row);
1999
+ const startCol = Math.min(selectionStart.col, selectionEnd?.col ?? selectionStart.col);
2000
+ if (onPaste) {
2001
+ onPaste(startRow, startCol, rows);
2002
+ }
2003
+ else if (onCellEdit) {
2004
+ rows.forEach((row, rIdx) => {
2005
+ row.forEach((cellValue, cIdx) => {
2006
+ const targetRow = startRow + rIdx;
2007
+ const targetCol = startCol + cIdx;
2008
+ if (targetRow < data.length && targetCol < columns.length) {
2009
+ const col = columns[targetCol];
2010
+ if (col.editable !== false) {
2011
+ let parsedValue = cellValue;
2012
+ if (col.dataType === 'number') {
2013
+ const num = parseFloat(cellValue.replace(/,/g, ''));
2014
+ parsedValue = isNaN(num) ? 0 : num;
2015
+ }
2016
+ onCellEdit(targetRow, col.key, parsedValue);
2017
+ }
2018
+ }
2019
+ });
2020
+ });
2021
+ }
2022
+ }).catch(console.error);
2023
+ }
2024
+ if (e.key === 'Delete' || e.key === 'Backspace') {
2025
+ if (!onCellEdit)
2026
+ return;
2027
+ e.preventDefault();
2028
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
2029
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
2030
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
2031
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
2032
+ for (let r = minRow; r <= maxRow; r++) {
2033
+ for (let c = minCol; c <= maxCol; c++) {
2034
+ const col = columns[c];
2035
+ if (col.editable !== false) {
2036
+ onCellEdit(r, col.key, '');
2037
+ }
2038
+ }
2039
+ }
2040
+ }
2041
+ if (e.key === 'Escape') {
2042
+ setSelectionStart(null);
2043
+ setSelectionEnd(null);
2044
+ setContextMenu({ visible: false, x: 0, y: 0 });
2045
+ }
2046
+ }, [enableRowSelection, enableKeyboardNavigation, selectedRowIndex, sortedData, onRowClick, selectionStart, selectionEnd, getSelectedData, onPaste, onCellEdit, data, columns]);
2047
+ // 표 밖을 클릭하면 셀 선택(타겟) 해제
2048
+ useEffect(() => {
2049
+ const handleMouseDownOutside = (event) => {
2050
+ const root = outerRef.current;
2051
+ const target = event.target;
2052
+ if (!root || !target)
2053
+ return;
2054
+ if (root.contains(target))
2055
+ return;
2056
+ setIsSelecting(false);
2057
+ setSelectionStart(null);
2058
+ setSelectionEnd(null);
2059
+ setEditingCell(null);
2060
+ setEditStartValue(null);
2061
+ setContextMenu({ visible: false, x: 0, y: 0 });
2062
+ };
2063
+ document.addEventListener('mousedown', handleMouseDownOutside, true);
2064
+ return () => {
2065
+ document.removeEventListener('mousedown', handleMouseDownOutside, true);
2066
+ };
2067
+ }, []);
2068
+ const handleContextMenu = useCallback((e) => {
2069
+ e.preventDefault();
2070
+ if (selectionStart && selectionEnd) {
2071
+ setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
2072
+ }
2073
+ }, [selectionStart, selectionEnd]);
2074
+ const closeContextMenu = useCallback(() => {
2075
+ setContextMenu({ visible: false, x: 0, y: 0 });
2076
+ }, []);
2077
+ const handleCopy = useCallback(() => {
2078
+ const text = getSelectedData();
2079
+ navigator.clipboard.writeText(text).catch(console.error);
2080
+ closeContextMenu();
2081
+ }, [getSelectedData, closeContextMenu]);
2082
+ const handlePaste = useCallback(() => {
2083
+ if (!selectionStart || !selectionEnd)
2084
+ return;
2085
+ navigator.clipboard.readText().then((text) => {
2086
+ if (!text)
2087
+ return;
2088
+ const rows = text
2089
+ .replace(/\r\n/g, '\n')
2090
+ .replace(/\r/g, '\n')
2091
+ .split('\n')
2092
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
2093
+ .map(row => row.split('\t'));
2094
+ const startRow = Math.min(selectionStart.row, selectionEnd.row);
2095
+ const startCol = Math.min(selectionStart.col, selectionEnd.col);
2096
+ if (onPaste) {
2097
+ onPaste(startRow, startCol, rows);
2098
+ }
2099
+ else if (onCellEdit) {
2100
+ rows.forEach((row, rIdx) => {
2101
+ row.forEach((cellValue, cIdx) => {
2102
+ const targetRow = startRow + rIdx;
2103
+ const targetCol = startCol + cIdx;
2104
+ if (targetRow < data.length && targetCol < columns.length) {
2105
+ const col = columns[targetCol];
2106
+ if (col.editable !== false) {
2107
+ let parsedValue = cellValue;
2108
+ if (col.dataType === 'number') {
2109
+ const num = parseFloat(cellValue.replace(/,/g, ''));
2110
+ parsedValue = isNaN(num) ? 0 : num;
2111
+ }
2112
+ onCellEdit(targetRow, col.key, parsedValue);
2113
+ }
2114
+ }
2115
+ });
2116
+ });
2117
+ }
2118
+ }).catch(console.error);
2119
+ closeContextMenu();
2120
+ }, [selectionStart, selectionEnd, onPaste, onCellEdit, data, columns, closeContextMenu]);
2121
+ const handleDelete = useCallback(() => {
2122
+ if (!selectionStart || !selectionEnd || !onCellEdit)
2123
+ return;
2124
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
2125
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
2126
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
2127
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
2128
+ for (let r = minRow; r <= maxRow; r++) {
2129
+ for (let c = minCol; c <= maxCol; c++) {
2130
+ const col = columns[c];
2131
+ if (col.editable !== false) {
2132
+ onCellEdit(r, col.key, '');
2133
+ }
2134
+ }
2135
+ }
2136
+ closeContextMenu();
2137
+ }, [selectionStart, selectionEnd, onCellEdit, columns, closeContextMenu]);
2138
+ const handleClearSelection = useCallback(() => {
2139
+ setSelectionStart(null);
2140
+ setSelectionEnd(null);
2141
+ closeContextMenu();
2142
+ }, [closeContextMenu]);
2143
+ const hasSelection = selectionStart !== null && selectionEnd !== null;
2144
+ const selectionCellCount = useMemo(() => {
2145
+ if (!selectionStart || !selectionEnd)
2146
+ return 0;
2147
+ const rows = Math.abs(selectionEnd.row - selectionStart.row) + 1;
2148
+ const cols = Math.abs(selectionEnd.col - selectionStart.col) + 1;
2149
+ return rows * cols;
2150
+ }, [selectionStart, selectionEnd]);
2151
+ useEffect(() => {
2152
+ const container = containerRef.current;
2153
+ if (!container)
2154
+ return;
2155
+ const handleMouseUp = () => {
2156
+ if (isSelecting) {
2157
+ handleCellMouseUp();
2158
+ }
2159
+ };
2160
+ document.addEventListener('mouseup', handleMouseUp);
2161
+ document.addEventListener('keydown', handleKeyDown);
2162
+ return () => {
2163
+ document.removeEventListener('mouseup', handleMouseUp);
2164
+ document.removeEventListener('keydown', handleKeyDown);
2165
+ };
2166
+ }, [isSelecting, handleCellMouseUp, handleKeyDown]);
2167
+ return (jsxs(TableOuterWrapper, { ref: outerRef, className: className, tabIndex: 0, onContextMenu: handleContextMenu, children: [jsx(TableWrapper, { children: jsx(TableContainer, { ref: containerRef, maxHeight: maxHeight, children: jsxs(StyledTable, { children: [jsx("colgroup", { children: columns.map((col) => (jsx("col", { style: col.width ? { width: col.width } : undefined }, String(col.key)))) }), jsx(TableHeader, { columns: columns, sortColumn: sortColumn ?? undefined, sortDirection: sortDirection, onSort: handleSort }), jsx(TableBody, { columns: columns, data: sortedData, rowHeight: rowHeight, onCellEdit: onCellEdit, selectionStart: enableRowSelection ? null : selectionStart, selectionEnd: enableRowSelection ? null : selectionEnd, editingCell: enableRowSelection ? null : editingCell, editStartValue: editStartValue, editToken: editToken, onCellMouseDown: enableRowSelection ? undefined : handleCellMouseDown, onCellMouseEnter: enableRowSelection ? undefined : handleCellMouseEnter, onCellMouseUp: enableRowSelection ? undefined : handleCellMouseUp, enableRowSelection: enableRowSelection, selectedRowIndex: selectedRowIndex, hoveredRowIndex: hoveredRowIndex ?? undefined, onRowClick: (rowIndex) => onRowClick?.(rowIndex, sortedData[rowIndex]), onRowHover: setHoveredRowIndex })] }) }) }), maxHeight && (jsxs(ScrollContainer, { children: [jsx(ScrollButton, { position: "top", onClick: () => scroll(-100), children: jsx(ChevronUp, {}) }), jsx(ScrollButton, { position: "bottom", onClick: () => scroll(100), children: jsx(ChevronDown, {}) })] })), contextMenu.visible && (jsxs(Fragment, { children: [jsx(ContextMenuOverlay, { onClick: closeContextMenu }), jsxs(ContextMenu, { x: contextMenu.x, y: contextMenu.y, children: [jsxs(ContextMenuItem, { onClick: handleCopy, disabled: !hasSelection, children: [jsx(Copy, { size: 14 }), "\uBCF5\uC0AC", jsx("span", { className: "shortcut", children: "\u2318C" })] }), jsxs(ContextMenuItem, { onClick: handlePaste, disabled: !hasSelection, children: [jsx(ClipboardPaste, { size: 14 }), "\uBD99\uC5EC\uB123\uAE30", jsx("span", { className: "shortcut", children: "\u2318V" })] }), jsx(ContextMenuDivider, {}), jsxs(ContextMenuItem, { onClick: handleDelete, disabled: !hasSelection || !onCellEdit, children: [jsx(Trash2, { size: 14 }), "\uC0AD\uC81C", jsx("span", { className: "shortcut", children: "Del" })] }), jsx(ContextMenuDivider, {}), jsxs(ContextMenuItem, { onClick: handleClearSelection, disabled: !hasSelection, children: [jsx(XCircle, { size: 14 }), "\uC120\uD0DD \uD574\uC81C", jsx("span", { className: "shortcut", children: "Esc" })] }), hasSelection && (jsxs(Fragment, { children: [jsx(ContextMenuDivider, {}), jsxs(ContextMenuItem, { disabled: true, style: { fontSize: '11px', color: '#9ca3af' }, children: [selectionCellCount, "\uAC1C \uC140 \uC120\uD0DD\uB428"] })] }))] })] }))] }));
2168
+ }
2169
+
2170
+ export { Button, CheckBox, Dropdown, Input, Sidebar, TabBar, Table, colors$1 as colors, typography };
1361
2171
  //# sourceMappingURL=index.esm.js.map