@rencar-dev/feature-modules-public 0.0.7 → 1.1.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/README.md CHANGED
@@ -10,206 +10,101 @@ npm install @rencar-dev/feature-modules-public
10
10
  yarn add @rencar-dev/feature-modules-public
11
11
  ```
12
12
 
13
- ## 사용법
13
+ ## 모듈 목록
14
14
 
15
- ```typescript
16
- // hooks 모듈 가져오기
17
- import { useExample } from "@rencar-dev/feature-modules-public/hooks";
18
-
19
- function MyComponent() {
20
- const { count, increment, decrement, reset } = useExample(0);
21
-
22
- return (
23
- <div>
24
- <p>Count: {count}</p>
25
- <button onClick={increment}>+</button>
26
- <button onClick={decrement}>-</button>
27
- <button onClick={reset}>Reset</button>
28
- </div>
29
- );
30
- }
31
- ```
32
-
33
- ## Presence 모듈
34
-
35
- 실시간 사용자 프레즌스(현재 접속자) 기능을 위한 모듈입니다.
36
-
37
- ### Socket 초기화
38
-
39
- 프레즌스 훅을 사용하기 전에 반드시 소켓을 초기화해야 합니다.
40
-
41
- #### initPresenceSocket
42
-
43
- 소켓 연결을 설정합니다. **앱 시작 시 1회 호출**해야 합니다.
44
-
45
- ```tsx
46
- import { initPresenceSocket } from "@rencar-dev/feature-modules-public/presence";
47
-
48
- // 앱 초기화 시 (예: App.tsx 또는 index.tsx)
49
- initPresenceSocket({
50
- url: "https://your-socket-server.com",
51
- transports: ["websocket"], // 선택사항, 기본값: ["websocket"]
52
- });
53
- ```
54
-
55
- **PresenceConfig:**
56
-
57
- | 옵션 | 타입 | 필수 | 기본값 | 설명 |
58
- | ------------ | ------------------------------ | ---- | --------------- | ------------- |
59
- | `url` | `string` | ✅ | - | 소켓 서버 URL |
60
- | `transports` | `("websocket" \| "polling")[]` | - | `["websocket"]` | 전송 방식 |
61
-
62
- ---
63
-
64
- #### getPresenceSocket
65
-
66
- 공유 소켓 인스턴스를 반환합니다. 초기화되지 않은 경우 에러를 발생시킵니다.
67
-
68
- ```tsx
69
- import { getPresenceSocket } from "@rencar-dev/feature-modules-public/presence";
70
-
71
- const socket = getPresenceSocket();
72
- socket.emit("custom-event", { data: "example" });
73
- ```
74
-
75
- ---
76
-
77
- #### disconnectPresenceSocket
78
-
79
- 소켓 연결을 종료하고 정리합니다. 앱 종료 시 또는 로그아웃 시 호출할 수 있습니다.
15
+ | 모듈 | 설명 | 문서 |
16
+ | -------- | ---------------------- | ------------------------------------ |
17
+ | hooks | 유틸리티 React 훅 | [docs/hooks.md](docs/hooks.md) |
18
+ | presence | 실시간 접속자 프레즌스 | [docs/presence.md](docs/presence.md) |
80
19
 
81
20
  ```tsx
82
- import { disconnectPresenceSocket } from "@rencar-dev/feature-modules-public/presence";
21
+ // hooks 모듈 import
22
+ import { useExample } from "@rencar-dev/feature-modules-public/hooks";
83
23
 
84
- // 로그아웃
85
- function handleLogout() {
86
- disconnectPresenceSocket();
87
- // ... 기타 로그아웃 로직
88
- }
24
+ // presence 모듈 import
25
+ import {
26
+ usePresence,
27
+ PresenceFloating,
28
+ } from "@rencar-dev/feature-modules-public/presence";
29
+ import "@rencar-dev/feature-modules-public/presence/styles.css";
89
30
  ```
90
31
 
91
32
  ---
92
33
 
93
- #### isPresenceSocketInitialized
34
+ ## 개발 가이드
94
35
 
95
- 소켓 초기화 여부를 확인합니다.
36
+ ### 로컬 개발
96
37
 
97
- ```tsx
98
- import { isPresenceSocketInitialized } from "@rencar-dev/feature-modules-public/presence";
38
+ ```bash
39
+ # 개발 모드 (watch)
40
+ npm run dev
99
41
 
100
- if (!isPresenceSocketInitialized()) {
101
- initPresenceSocket({ url: "..." });
102
- }
42
+ # 빌드
43
+ npm run build
103
44
  ```
104
45
 
105
- ---
46
+ ### 새 모듈 추가하기
106
47
 
107
- ## Presence Hooks
48
+ 1. **폴더 생성**: `src/[모듈명]/` 폴더 생성
49
+ 2. **Entry 파일**: `src/[모듈명]/index.ts` 생성 및 export 정의
50
+ 3. **tsup 설정**: `tsup.config.ts`에 entry 추가
108
51
 
109
- 실시간 사용자 프레즌스(현재 접속자) 기능을 위한 훅입니다.
52
+ ```ts
53
+ entry: {
54
+ "hooks/index": "src/hooks/index.ts",
55
+ "presence/index": "src/presence/index.ts",
56
+ "[모듈명]/index": "src/[모듈명]/index.ts", // 추가
57
+ },
58
+ ```
110
59
 
111
- ### usePresence
60
+ 4. **package.json exports 추가**:
112
61
 
113
- 특정 룸에 참여하고 해당 룸의 사용자 목록을 추적합니다. 현재 사용자가 해당 룸에 참여자로 등록됩니다.
62
+ ```json
63
+ "./[모듈명]": {
64
+ "types": "./dist/[모듈명]/index.d.ts",
65
+ "import": "./dist/[모듈명]/index.js",
66
+ "require": "./dist/[모듈명]/index.cjs"
67
+ }
68
+ ```
114
69
 
115
- **Options:**
70
+ 5. **typesVersions 추가**:
116
71
 
117
- | 옵션 | 타입 | 필수 | 기본값 | 설명 |
118
- | ------------------- | -------------- | ---- | --------------- | -------------------------------------------------- |
119
- | `roomId` | `string` | ✅ | - | 참여할 룸 ID |
120
- | `currentUser` | `PresenceUser` | ✅ | - | 현재 사용자 정보 (`{ userId, name, ...추가속성 }`) |
121
- | `enabled` | `boolean` | - | `true` | 훅 활성화 여부 |
122
- | `heartbeatInterval` | `number` | - | `600000` (10분) | 하트비트 간격 (ms) |
72
+ ```json
73
+ "[모듈명]": ["./dist/[모듈명]/index.d.ts"]
74
+ ```
123
75
 
124
- **Returns:** `PresenceUser[]` - 현재 룸에 있는 사용자 목록
76
+ 6. **문서 작성**: `docs/[모듈명].md` 생성
77
+ 7. **README 업데이트**: 모듈 목록 테이블에 추가
125
78
 
126
- **사용 예시:**
79
+ ### 배포
127
80
 
128
- ```tsx
129
- import { usePresence } from "@rencar-dev/feature-modules-public/presence";
130
-
131
- function MyPage() {
132
- const users = usePresence({
133
- roomId: "my-app:page-123",
134
- currentUser: { userId: "1", name: "John Doe" },
135
- });
136
-
137
- return (
138
- <div>
139
- <p>현재 접속자: {users.length}명</p>
140
- <ul>
141
- {users.map((user) => (
142
- <li key={user.userId}>{user.name}</li>
143
- ))}
144
- </ul>
145
- </div>
146
- );
147
- }
148
- ```
81
+ **자동 배포 (권장):**
149
82
 
150
- **조건부 활성화:**
83
+ `master` 브랜치에 push하면 semantic-release가 자동으로:
151
84
 
152
- ```tsx
153
- // 로그인한 경우에만 프레즌스 활성화
154
- const users = usePresence({
155
- roomId: "my-room",
156
- currentUser: { userId: user.id, name: user.name },
157
- enabled: isLoggedIn,
158
- });
159
- ```
85
+ 1. 커밋 메시지 분석 (`fix:`, `feat:` 등)
86
+ 2. 버전 번호 결정 및 package.json 업데이트
87
+ 3. Git 태그 생성
88
+ 4. npm 배포
89
+ 5. GitHub Release 생성
160
90
 
161
- ---
162
-
163
- ### usePresenceWatch
164
-
165
- 여러 룸을 **참여 없이** 관찰(watch)합니다. 읽기 전용으로 다른 룸들의 접속자를 모니터링할 때 사용합니다.
91
+ | 커밋 접두사 | 버전 변경 | 예시 |
92
+ | ----------- | ------------- | ------------------------ |
93
+ | `fix:` | patch (0.0.X) | `fix: 버그 수정` |
94
+ | `feat:` | minor (0.X.0) | `feat: 새 기능 추가` |
95
+ | `feat!:` | major (X.0.0) | `feat!: breaking change` |
166
96
 
167
- **Options:**
97
+ > **Note**: `chore:`, `docs:`, `style:` 등은 릴리즈를 트리거하지 않습니다.
168
98
 
169
- | 옵션 | 타입 | 필수 | 기본값 | 설명 |
170
- | --------- | ---------- | ---- | ------ | ----------------- |
171
- | `roomIds` | `string[]` | ✅ | - | 관찰할 룸 ID 배열 |
172
- | `enabled` | `boolean` | - | `true` | 훅 활성화 여부 |
99
+ **수동 배포:**
173
100
 
174
- **Returns:** `Record<string, PresenceUser[]>` - 룸 ID → 사용자 목록 맵
175
-
176
- **사용 예시:**
177
-
178
- ```tsx
179
- import { usePresenceWatch } from "@rencar-dev/feature-modules-public/presence";
180
-
181
- function Dashboard() {
182
- const roomsUsers = usePresenceWatch({
183
- roomIds: ["page-1", "page-2", "page-3"],
184
- });
185
-
186
- return (
187
- <div>
188
- {Object.entries(roomsUsers).map(([roomId, users]) => (
189
- <div key={roomId}>
190
- <h3>{roomId}</h3>
191
- <p>{users.length}명 접속 중</p>
192
- </div>
193
- ))}
194
- </div>
195
- );
196
- }
197
- ```
198
-
199
- **동적 룸 ID:**
200
-
201
- ```tsx
202
- // 룸 ID가 변경되면 자동으로 watch/unwatch 처리
203
- const [selectedRooms, setSelectedRooms] = useState(["room-a"]);
204
- const roomsUsers = usePresenceWatch({ roomIds: selectedRooms });
101
+ ```bash
102
+ npm version patch # 또는 minor, major
103
+ npm publish
205
104
  ```
206
105
 
207
106
  ---
208
107
 
209
- ### usePresence vs usePresenceWatch 비교
108
+ ## 라이선스
210
109
 
211
- | 항목 | usePresence | usePresenceWatch |
212
- | ----------- | -------------------------------- | ----------------------------- |
213
- | **룸 참여** | ✅ 참여함 (다른 사용자에게 보임) | ❌ 참여 안함 (관찰만) |
214
- | **룸 개수** | 1개 | 여러 개 |
215
- | **용도** | 현재 페이지 접속자 표시 | 대시보드에서 여러 룸 모니터링 |
110
+ MIT
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/presence/index.ts
21
21
  var presence_exports = {};
22
22
  __export(presence_exports, {
23
+ PresenceAvatars: () => PresenceAvatars,
24
+ PresenceFloating: () => PresenceFloating,
23
25
  disconnectPresenceSocket: () => disconnectPresenceSocket,
24
26
  getPresenceSocket: () => getPresenceSocket,
25
27
  initPresenceSocket: () => initPresenceSocket,
@@ -243,7 +245,7 @@ function usePresenceAvatarsState(options) {
243
245
  const [hoveredIndex, setHoveredIndex] = (0, import_react3.useState)(null);
244
246
  const hoveredUser = hoveredIndex != null ? visibleUsers[hoveredIndex] : null;
245
247
  const baseTopZ = visibleUsers.length + 1;
246
- const getInitial = (0, import_react3.useCallback)((user) => {
248
+ const getInitial2 = (0, import_react3.useCallback)((user) => {
247
249
  return getInitialFromUser(user);
248
250
  }, []);
249
251
  const getZIndex = (0, import_react3.useCallback)(
@@ -258,7 +260,7 @@ function usePresenceAvatarsState(options) {
258
260
  hoveredUser,
259
261
  hoveredIndex,
260
262
  setHoveredIndex,
261
- getInitial,
263
+ getInitial: getInitial2,
262
264
  getZIndex
263
265
  };
264
266
  }
@@ -445,8 +447,234 @@ function usePresenceFloatingState(options) {
445
447
  onAvatarLeave
446
448
  };
447
449
  }
450
+
451
+ // src/presence/components/PresenceAvatars.tsx
452
+ var import_react5 = require("react");
453
+ var import_jsx_runtime = require("react/jsx-runtime");
454
+ function PresenceAvatars({
455
+ users,
456
+ maxVisible = 3,
457
+ className,
458
+ renderAvatar,
459
+ renderTooltip,
460
+ renderMore
461
+ }) {
462
+ const {
463
+ visibleUsers,
464
+ moreCount,
465
+ hoveredUser,
466
+ hoveredIndex,
467
+ setHoveredIndex,
468
+ getInitial: getInitial2,
469
+ getZIndex
470
+ } = usePresenceAvatarsState({ users, maxVisible });
471
+ const [tooltipPos, setTooltipPos] = (0, import_react5.useState)(null);
472
+ const updateTooltipPos = (0, import_react5.useCallback)((el) => {
473
+ const rect = el.getBoundingClientRect();
474
+ const tooltipWidth = 200;
475
+ const half = tooltipWidth / 2;
476
+ const centerX = rect.left + rect.width / 2;
477
+ const left = Math.min(
478
+ Math.max(centerX, half + 8),
479
+ window.innerWidth - half - 8
480
+ );
481
+ const top = Math.min(rect.bottom + 8, window.innerHeight - 8);
482
+ setTooltipPos({ top, left });
483
+ }, []);
484
+ const handleMouseEnter = (0, import_react5.useCallback)(
485
+ (e, idx) => {
486
+ setHoveredIndex(idx);
487
+ updateTooltipPos(e.currentTarget);
488
+ },
489
+ [setHoveredIndex, updateTooltipPos]
490
+ );
491
+ const handleMouseLeave = (0, import_react5.useCallback)(() => {
492
+ setHoveredIndex(null);
493
+ }, [setHoveredIndex]);
494
+ const defaultTooltipRenderer = (0, import_react5.useCallback)(
495
+ ({ user, position }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
496
+ "div",
497
+ {
498
+ className: "presence-avatars__tooltip",
499
+ style: { top: position.top, left: position.left },
500
+ children: [
501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "presence-avatars__tooltip-row", children: [
502
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "presence-avatars__tooltip-key", children: "Name" }),
503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "presence-avatars__tooltip-val", children: user.name || "-" })
504
+ ] }),
505
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "presence-avatars__tooltip-row", children: [
506
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "presence-avatars__tooltip-key", children: "ID" }),
507
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "presence-avatars__tooltip-val", children: user.userId || "-" })
508
+ ] })
509
+ ]
510
+ }
511
+ ),
512
+ []
513
+ );
514
+ const defaultAvatarRenderer = (0, import_react5.useCallback)(
515
+ ({ initial, isHovered }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: initial }),
516
+ []
517
+ );
518
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `presence-avatars ${className ?? ""}`, children: [
519
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "presence-avatars__stack", children: [
520
+ visibleUsers.map((user, idx) => {
521
+ const isHovered = hoveredIndex === idx;
522
+ const initial = getInitial2(user);
523
+ const zIndex = getZIndex(idx, isHovered);
524
+ const renderProps = {
525
+ user,
526
+ index: idx,
527
+ initial,
528
+ isHovered,
529
+ zIndex
530
+ };
531
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
532
+ "span",
533
+ {
534
+ className: `presence-avatars__avatar ${isHovered ? "is-hovered" : ""}`,
535
+ style: { zIndex },
536
+ onMouseEnter: (e) => handleMouseEnter(e, idx),
537
+ onMouseLeave: handleMouseLeave,
538
+ "aria-label": user.name || user.userId || "",
539
+ children: renderAvatar ? renderAvatar(renderProps) : defaultAvatarRenderer(renderProps)
540
+ },
541
+ `${user.userId}-${idx}`
542
+ );
543
+ }),
544
+ moreCount > 0 && (renderMore ? renderMore(moreCount) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "presence-avatars__more", children: [
545
+ "+",
546
+ moreCount
547
+ ] }))
548
+ ] }),
549
+ hoveredUser && tooltipPos && (renderTooltip ? renderTooltip({ user: hoveredUser, position: tooltipPos }) : defaultTooltipRenderer({
550
+ user: hoveredUser,
551
+ position: tooltipPos
552
+ }))
553
+ ] });
554
+ }
555
+
556
+ // src/presence/components/PresenceFloating.tsx
557
+ var import_react6 = require("react");
558
+ var import_jsx_runtime2 = require("react/jsx-runtime");
559
+ function getInitial(user) {
560
+ const base = (user?.name || user?.userId || "").trim();
561
+ if (!base) return "?";
562
+ const ch = base.charAt(0);
563
+ return /[a-z]/i.test(ch) ? ch.toUpperCase() : ch;
564
+ }
565
+ function PresenceFloating({
566
+ users,
567
+ maxVisible = 8,
568
+ className,
569
+ style,
570
+ initialPosition,
571
+ title = "\uC5F4\uB78C\uC911",
572
+ emptyText = "\uC5C6\uC74C",
573
+ moreText = (n) => `+${n}\uBA85`,
574
+ renderAvatar,
575
+ renderTooltip
576
+ }) {
577
+ const {
578
+ containerRef,
579
+ inlineStyle,
580
+ isDragging,
581
+ visibleUsers,
582
+ moreCount,
583
+ hoveredUser,
584
+ tooltipTop,
585
+ onMouseDownHeader,
586
+ onTouchStartHeader,
587
+ onAvatarEnter,
588
+ onAvatarLeave
589
+ } = usePresenceFloatingState({ users, maxVisible, initialPosition });
590
+ const defaultTooltipRenderer = (0, import_react6.useCallback)(
591
+ ({ user, top }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
592
+ "div",
593
+ {
594
+ className: "presence-floating__tooltip",
595
+ style: { top: Math.max(0, top - 6) },
596
+ onMouseLeave: onAvatarLeave,
597
+ children: [
598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "presence-floating__tooltip-row", children: [
599
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__tooltip-key", children: "Name" }),
600
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__tooltip-val", children: user.name || "-" })
601
+ ] }),
602
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "presence-floating__tooltip-row", children: [
603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__tooltip-key", children: "ID" }),
604
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__tooltip-val", children: user.userId || "-" })
605
+ ] })
606
+ ]
607
+ }
608
+ ),
609
+ [onAvatarLeave]
610
+ );
611
+ const defaultAvatarRenderer = (0, import_react6.useCallback)(
612
+ ({ initial }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: initial }),
613
+ []
614
+ );
615
+ const combinedStyle = {
616
+ ...inlineStyle,
617
+ ...style
618
+ };
619
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
620
+ "div",
621
+ {
622
+ ref: containerRef,
623
+ className: `presence-floating ${isDragging ? "is-dragging" : ""} ${className ?? ""}`,
624
+ style: combinedStyle,
625
+ children: [
626
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
627
+ "div",
628
+ {
629
+ className: "presence-floating__header",
630
+ onMouseDown: onMouseDownHeader,
631
+ onTouchStart: onTouchStartHeader,
632
+ children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
634
+ "span",
635
+ {
636
+ className: "presence-floating__title",
637
+ title,
638
+ "aria-label": title,
639
+ children: title
640
+ }
641
+ ),
642
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__count", children: users.length })
643
+ ]
644
+ }
645
+ ),
646
+ hoveredUser && (renderTooltip ? renderTooltip({ user: hoveredUser, top: tooltipTop }) : defaultTooltipRenderer({ user: hoveredUser, top: tooltipTop })),
647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "presence-floating__body", children: [
648
+ visibleUsers.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__empty", children: emptyText }) : visibleUsers.map((user, idx) => {
649
+ const initial = getInitial(user);
650
+ const label = user.name || user.userId || "";
651
+ const renderProps = {
652
+ user,
653
+ initial
654
+ };
655
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
656
+ "span",
657
+ {
658
+ className: "presence-floating__avatar",
659
+ title: label,
660
+ "aria-label": label,
661
+ onMouseEnter: (e) => onAvatarEnter(e, user),
662
+ onMouseLeave: onAvatarLeave,
663
+ children: renderAvatar ? renderAvatar(renderProps) : defaultAvatarRenderer(renderProps)
664
+ },
665
+ `${user.userId}-${idx}`
666
+ );
667
+ }),
668
+ moreCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "presence-floating__more", children: moreText(moreCount) })
669
+ ] })
670
+ ]
671
+ }
672
+ );
673
+ }
448
674
  // Annotate the CommonJS export names for ESM import in node:
449
675
  0 && (module.exports = {
676
+ PresenceAvatars,
677
+ PresenceFloating,
450
678
  disconnectPresenceSocket,
451
679
  getPresenceSocket,
452
680
  initPresenceSocket,