axmap-cli 1.2.3 → 1.2.5
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/bin/axmap.mjs +68 -3
- package/governance/vote.mjs +64 -3
- package/package.json +1 -1
- package/src/governance.mjs +23 -0
- package/src/update.mjs +20 -2
package/bin/axmap.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
noticeLine,
|
|
44
44
|
registryUrl,
|
|
45
45
|
} from '../src/update.mjs'
|
|
46
|
+
import { stateDir } from '../src/repotarget.mjs'
|
|
46
47
|
import { homeRegistrations } from '../src/mcpstate.mjs'
|
|
47
48
|
import { compareBranchFor, formatOverlap } from '../src/overlap.mjs'
|
|
48
49
|
|
|
@@ -2612,6 +2613,19 @@ function cmdRun(name) {
|
|
|
2612
2613
|
* 아래에 찍히는 한 줄을 직접 실행하는 것으로만 일어난다.
|
|
2613
2614
|
*
|
|
2614
2615
|
* 종료 코드: 0 물어봤다 (최신이든 아니든) · 1 못 물어봤다
|
|
2616
|
+
*
|
|
2617
|
+
* 🔴 **이 명령만은 `process.exit()` 로 끝내지 않는다 (2026-09-10).**
|
|
2618
|
+
*
|
|
2619
|
+
* 레지스트리에 묻는 순간 fetch 가 소켓을 연다. 답을 다 읽은 뒤에도 그 소켓은
|
|
2620
|
+
* 아직 **닫히는 중**이고, 그 상태에서 `process.exit()` 를 부르면 Windows 의
|
|
2621
|
+
* node 가 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)` 로 스스로
|
|
2622
|
+
* 중단한다. 화면에는 "최신입니다" 가 이미 찍힌 뒤라 **사람 눈에는 정상인데
|
|
2623
|
+
* 종료 코드가 127** 이다. 판정을 종료 코드로 하는 이 저장소에서 그건 조용한
|
|
2624
|
+
* 실패고, 이 명령을 검사에 넣는 순간 언제나 빨개진다.
|
|
2625
|
+
*
|
|
2626
|
+
* 한 틱 미루는 것(`setImmediate`)으로는 안 된다 — 재봤을 때 똑같이 죽었다.
|
|
2627
|
+
* 이벤트 루프가 실제로 비워질 때까지 기다려야 한다. 그래서 `process.exitCode`
|
|
2628
|
+
* 만 세우고 돌아간다. 느려지지 않는다 (둘 다 230ms 안쪽이었다).
|
|
2615
2629
|
*/
|
|
2616
2630
|
async function cmdUpdate(flags) {
|
|
2617
2631
|
const current = selfVersion()
|
|
@@ -2643,23 +2657,74 @@ async function cmdUpdate(flags) {
|
|
|
2643
2657
|
console.error(`새 버전을 확인하지 못했습니다: ${e.message}`)
|
|
2644
2658
|
console.error('네트워크가 안 되거나, 아직 배포된 적이 없는 꾸러미일 수 있습니다.')
|
|
2645
2659
|
console.error('')
|
|
2646
|
-
process.
|
|
2660
|
+
process.exitCode = 1
|
|
2661
|
+
return
|
|
2647
2662
|
}
|
|
2648
2663
|
|
|
2649
2664
|
writeCache({ latest, current, checkedAt: Date.now(), registry })
|
|
2650
2665
|
|
|
2651
2666
|
const notice = noticeLine(current, latest)
|
|
2652
2667
|
console.log(`최신 버전: ${latest}`)
|
|
2668
|
+
|
|
2669
|
+
// 🔴 **훅이 부르는 사본도 같이 본다 (2026-09-12).**
|
|
2670
|
+
//
|
|
2671
|
+
// 판이 이 PC 에 **두 군데** 있다. `axmap` 명령(전역 설치)과 git 훅이 부르는
|
|
2672
|
+
// 사본(`~/.axmap/app`)이다. `npm i -g` 는 앞의 것만 올리고 뒤의 것은 `setup`
|
|
2673
|
+
// 이 올린다 — 서로 안 따라간다.
|
|
2674
|
+
//
|
|
2675
|
+
// 그런데 이 명령은 **자기 판만 보고** "최신입니다" 라고 답했다. 훅이 부르는
|
|
2676
|
+
// 사본이 옛 판인데도 그랬다. 막는 것보다 나쁘다 — **괜찮다고 말하고 넘어간다.**
|
|
2677
|
+
// 실측 (2026-09-12): `npm i -g` 로 전역이 1.2.3 이 된 직후 이 명령이
|
|
2678
|
+
// "최신입니다" 라고 했는데 훅이 부르는 사본은 1.2.2 였다.
|
|
2679
|
+
const installed = installedAppVersion()
|
|
2680
|
+
const skewed = Boolean(installed && current && installed !== current)
|
|
2681
|
+
if (skewed) {
|
|
2682
|
+
console.log('')
|
|
2683
|
+
console.log(`🔴 git 훅이 부르는 사본은 아직 ${installed} 입니다 (이 명령은 ${current}).`)
|
|
2684
|
+
console.log(' 둘은 따로 움직입니다 — 전역은 `npm i -g`, 사본은 `axmap setup` 이 올립니다.')
|
|
2685
|
+
console.log('')
|
|
2686
|
+
console.log(' 맞추려면: axmap setup')
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2653
2689
|
console.log('')
|
|
2654
2690
|
if (notice) {
|
|
2655
2691
|
for (const l of notice.split('\n')) console.log(l)
|
|
2656
2692
|
console.log('')
|
|
2657
|
-
console.log(' 🔴 저절로 바뀌지 않습니다. 위
|
|
2693
|
+
console.log(' 🔴 저절로 바뀌지 않습니다. 위 명령을 직접 실행할 때만 바뀝니다.')
|
|
2694
|
+
} else if (skewed) {
|
|
2695
|
+
// 여기서 "최신입니다" 라고 말하면 거짓이 된다. 이 PC 의 실력은 **가장 낡은 것**이다.
|
|
2696
|
+
console.log('레지스트리 기준으로는 최신이지만, 위 사본이 아직 안 맞습니다.')
|
|
2658
2697
|
} else {
|
|
2659
2698
|
console.log('최신입니다.')
|
|
2660
2699
|
}
|
|
2661
2700
|
console.log('')
|
|
2662
|
-
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
/**
|
|
2704
|
+
* git 훅이 실제로 부르는 사본(`~/.axmap/app`)의 판. 없거나 못 읽으면 `null`.
|
|
2705
|
+
*
|
|
2706
|
+
* 🔴 **`selfVersion()` 과 다른 것을 본다.** `selfVersion()` 은 *"지금 이 코드"* 의
|
|
2707
|
+
* 판이고, 이 함수는 *"커밋할 때마다 실제로 도는 코드"* 의 판이다. 둘이 갈릴 수
|
|
2708
|
+
* 있다는 것이 이 PC 구조의 사실이므로, 하나만 보고 답하면 거짓말이 된다.
|
|
2709
|
+
*
|
|
2710
|
+
* 🔴 자리를 여기서 새로 정하지 않는다 — `stateDir()` 을 쓴다. 그래야
|
|
2711
|
+
* `AXMAP_STATE_DIR` 이 여기에도 걸린다. 그 환경변수가 있는 이유가
|
|
2712
|
+
* `src/repotarget.mjs` 에 적혀 있다 — *"테스트가 사람의 작업 환경을 고치는
|
|
2713
|
+
* 것은 어떤 이유로도 정당하지 않다."* `os.homedir()` 를 직접 부르면 이 명령의
|
|
2714
|
+
* 출력이 **돌리는 사람의 PC 상태에 따라 달라지고**, 그러면 시험이 사람마다
|
|
2715
|
+
* 다른 답을 낸다 (실제로 그렇게 깨졌다).
|
|
2716
|
+
*
|
|
2717
|
+
* 🔴 못 읽으면 `null` 을 돌려 **아무 말도 안 한다.** 사본을 안 쓰는 설치도 있고
|
|
2718
|
+
* (팀 저장소 안의 벤더링 사본은 위에서 이미 걸러진다), 없는 것을 낡았다고
|
|
2719
|
+
* 말하면 그 경고는 곧 아무도 안 읽는 잡음이 된다.
|
|
2720
|
+
*/
|
|
2721
|
+
function installedAppVersion() {
|
|
2722
|
+
try {
|
|
2723
|
+
const p = path.join(stateDir(), 'app', 'package.json')
|
|
2724
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))?.version ?? null
|
|
2725
|
+
} catch {
|
|
2726
|
+
return null
|
|
2727
|
+
}
|
|
2663
2728
|
}
|
|
2664
2729
|
|
|
2665
2730
|
/**
|
package/governance/vote.mjs
CHANGED
|
@@ -419,8 +419,52 @@ function g1WaiverFor(branch, targetName, policyFlag) {
|
|
|
419
419
|
return { eligibleCount, threshold }
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
/**
|
|
423
|
+
* **판정에 물어본다 — "이 사람이 G1 으로 빠지나?"**
|
|
424
|
+
*
|
|
425
|
+
* 🔴 이것이 1.2.4 의 요점이다. 1.2.3 까지는 쓰는 쪽이 같은 규칙을 **자기가
|
|
426
|
+
* 계산**했고, 그 어긋남이 **두 번** 났다 — 2026-09-07 의 G1 해제(세는 쪽만
|
|
427
|
+
* 알았다), 2026-09-12 의 `self_vote` 모드(쓰는 쪽이 정책을 안 읽었다).
|
|
428
|
+
* 같은 규칙을 두 곳에 적으면 몇 번을 고쳐도 또 난다. **판정자는 하나여야 한다.**
|
|
429
|
+
*
|
|
430
|
+
* 🔴 **`null` 은 "모른다" 다 — 통과도 거부도 아니다.** 게이트를 못 돌린 경우
|
|
431
|
+
* (저장소가 아직 그럴 상태가 아니거나, 정책이 없거나, 타임아웃)에는 부르는
|
|
432
|
+
* 쪽이 직접 보는 길로 떨어진다. 여기서 `false` 를 돌려 조용히 통과시키면,
|
|
433
|
+
* 게이트가 잠깐 안 도는 것만으로 G1 이 꺼진다.
|
|
434
|
+
*
|
|
435
|
+
* 🔴 **해제와 섞지 않는다.** 이 함수가 답하는 것은 *"자기 표인가"* 하나다.
|
|
436
|
+
* *"자기 표인데 그래도 세는가"* 는 `g1WaiverFor` 가 따로 답한다. 둘을 합치면
|
|
437
|
+
* 부르는 쪽이 **"자기 표가 아니다" 와 "자기 표인데 해제됐다" 를 구별 못 하고**,
|
|
438
|
+
* 그러면 해제로 통과한 사람에게 왜 그냥 써 줬는지 말해 줄 수 없다.
|
|
439
|
+
*
|
|
440
|
+
* @returns {{self:boolean, mode:string|null}|null} 못 물어봤으면 `null`
|
|
441
|
+
*/
|
|
442
|
+
function selfVoteByGate(branch, targetName, email, policyFlag) {
|
|
443
|
+
const v = runGate(branch, targetName, policyFlag)
|
|
444
|
+
if (!v || v.stage !== 'count' || !v.tally) return null
|
|
445
|
+
const list = v.tally.selfVoteEmails
|
|
446
|
+
// 옛 게이트(1.2.3 이하)는 이 칸을 안 낸다. 없는 것을 "자기 표가 아니다" 로
|
|
447
|
+
// 읽으면 판이 섞인 팀에서 G1 이 조용히 꺼진다 — 모른다고 답한다.
|
|
448
|
+
if (!Array.isArray(list)) return null
|
|
449
|
+
return {
|
|
450
|
+
self: list.includes(String(email ?? '').trim().toLowerCase()),
|
|
451
|
+
mode: typeof v.tally.selfVoteMode === 'string' ? v.tally.selfVoteMode : null,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
422
455
|
function warnIfSelfVote(branch, targetName, email, force, policyFlag, policy) {
|
|
423
|
-
//
|
|
456
|
+
// ── 1. 먼저 판정에 물어본다 ──────────────────────────────────────────────
|
|
457
|
+
const asked = selfVoteByGate(branch, targetName, email, policyFlag)
|
|
458
|
+
if (asked && !asked.self) return // 판정이 "자기 표가 아니다" 고 했다. 끝.
|
|
459
|
+
if (asked && asked.self) {
|
|
460
|
+
return reportSelfVote(branch, targetName, email, force, policyFlag, {
|
|
461
|
+
label: `${targetName} 기준 판정`,
|
|
462
|
+
mode: asked.mode,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── 2. 못 물어봤으면 직접 본다 ───────────────────────────────────────────
|
|
467
|
+
// 🔴 **세는 쪽과 같은 함수로** 정책을 읽는다. 여기서 조건을 다시 적으면 어긋난다.
|
|
424
468
|
const mode = selfVoteModeSafe(policy)
|
|
425
469
|
|
|
426
470
|
// `off` 는 G1 자체를 끈 것이다. 막을 것도 경고할 것도 없다.
|
|
@@ -440,6 +484,21 @@ function warnIfSelfVote(branch, targetName, email, force, policyFlag, policy) {
|
|
|
440
484
|
const authors = new Set(r.out.split('\n').map((s) => s.trim().toLowerCase()).filter(Boolean))
|
|
441
485
|
if (!authors.has(email.toLowerCase())) return
|
|
442
486
|
|
|
487
|
+
reportSelfVote(branch, targetName, email, force, policyFlag, { label: range.label, mode })
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* **자기 표인 것이 확인된 뒤**, 그것을 사람에게 어떻게 말하고 막을지.
|
|
492
|
+
*
|
|
493
|
+
* 🔴 두 길(판정에 물어본 길·직접 본 길)이 **같은 코드로 끝나야 한다.** 여기를
|
|
494
|
+
* 두 벌로 두면 "판정이 답한 날" 과 "게이트를 못 돌린 날" 의 안내가 갈린다 —
|
|
495
|
+
* 그 갈라짐이 이 파일에서 이미 두 번 사고를 냈다.
|
|
496
|
+
*
|
|
497
|
+
* @param {{label:string, mode:string}|null} seen 직접 본 길만 채운다. 판정에
|
|
498
|
+
* 물어본 길에서는 `null` — 그때는 무엇을 봤는지 이 파일이 모르고,
|
|
499
|
+
* **모르는 것을 지어내 적지 않는다.**
|
|
500
|
+
*/
|
|
501
|
+
function reportSelfVote(branch, targetName, email, force, policyFlag, seen) {
|
|
443
502
|
// 자기 표는 맞다. 그런데 **이 판정에서는 세어지는가?** 세는 쪽에 물어본다.
|
|
444
503
|
const waiver = g1WaiverFor(branch, targetName, policyFlag)
|
|
445
504
|
if (waiver) {
|
|
@@ -454,8 +513,10 @@ function warnIfSelfVote(branch, targetName, email, force, policyFlag, policy) {
|
|
|
454
513
|
|
|
455
514
|
console.error(
|
|
456
515
|
`이 표는 자기 표라 세지지 않습니다 (G1 — 자기가 쓴 코드에 자기가 찬성할 수 없습니다).\n`
|
|
457
|
-
+
|
|
458
|
-
|
|
516
|
+
+ (seen
|
|
517
|
+
? ` ${email} 이(가) ${seen.label} 의 author 목록에 있습니다.\n`
|
|
518
|
+
+ ` 배제 범위는 정책의 self_vote = "${seen.mode}" 가 정합니다.\n\n`
|
|
519
|
+
: ` ${email} 을(를) ${targetName} 기준 판정이 자기 표로 셉니다.\n\n`)
|
|
459
520
|
+ ' 그래도 남기려면 --force 를 붙이세요. 파일은 남고 판정문에는 "안 센 표" 로 보입니다.\n'
|
|
460
521
|
+ ' 조용히 무효표를 쌓지 않는 이유: 던졌다고 믿는 사람이 아무도 안 세는 표를 기다립니다.',
|
|
461
522
|
)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//name": "🔴 npm 의 'axmap' 은 2022년부터 남이 쓰고 있는 다른 꾸러미다(leozin_ 의 Map 라이브러리, 1.1.2). 그래서 `npx axmap` 은 우리 것이 아니고, 누구에게도 그렇게 안내하면 안 된다. 2026-08-28 에 axmap-cli 로 정했다 — 스코프(@사용자/이름)를 쓰지 않는 이유는 스코프가 npm 계정 이름에 묶여서 나중에 팀이나 조직으로 옮길 때 이름이 통째로 바뀌기 때문이다. 사람이 치는 **명령** 이름은 그대로 axmap 이다 (아래 bin) — 꾸러미 이름과 명령 이름은 달라도 된다.",
|
|
3
3
|
"name": "axmap-cli",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.5",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|
package/src/governance.mjs
CHANGED
|
@@ -1064,6 +1064,29 @@ export function countVotes({
|
|
|
1064
1064
|
eligibleCount,
|
|
1065
1065
|
threshold: need,
|
|
1066
1066
|
selfVoted,
|
|
1067
|
+
// ── 🔴 G1 이 배제하는 email 들 (2026-09-12) ──────────────────────────
|
|
1068
|
+
//
|
|
1069
|
+
// 쓰는 쪽(`governance/vote.mjs`)이 *"내 표가 세어지나"* 를 **여기에 물어볼 수
|
|
1070
|
+
// 있게** 한다. 그러지 않으면 쓰는 쪽이 같은 규칙을 자기가 계산해야 하고,
|
|
1071
|
+
// 그 어긋남이 실제로 **두 번** 났다 — 2026-09-07 의 G1 해제, 2026-09-12 의
|
|
1072
|
+
// `self_vote` 모드. 판정자는 하나여야 한다.
|
|
1073
|
+
//
|
|
1074
|
+
// 🔴 **명단으로 거르지 않는다.** 여기 담기는 것은 `excludedAuthors` 가 낸
|
|
1075
|
+
// 집합 그대로다. 명단으로 좁히면 명단 밖 사람에게는 경고가 안 가고, 그
|
|
1076
|
+
// 사람은 **아무도 안 세는 표를 던져 놓고 기다리게 된다** — 이 저장소가
|
|
1077
|
+
// 자기 표를 조용히 쌓지 않기로 한 이유가 그것이다.
|
|
1078
|
+
// (명단에 있나 없나는 `warnIfNotAVoter` 가 따로, 막지 않고 알린다.)
|
|
1079
|
+
//
|
|
1080
|
+
// 🔴 **해제(`g1Waived`)가 걸려도 비우지 않는다.** 이 칸은 *"이 판정에서
|
|
1081
|
+
// 누가 자기 표인가"* 이고, 해제는 *"그 자기 표를 그래도 세는가"* 다.
|
|
1082
|
+
// 둘은 다른 사실이라 따로 내야 한다 — 합쳐서 비워 버리면 읽는 쪽이
|
|
1083
|
+
// "자기 표가 아니다" 와 "자기 표인데 해제됐다" 를 **구별할 수 없고**,
|
|
1084
|
+
// 그러면 해제로 통과한 사람에게 *왜* 그냥 써 줬는지 말해 줄 수 없다.
|
|
1085
|
+
// (읽는 쪽은 이 칸과 `g1Waived` 를 같이 본다.)
|
|
1086
|
+
selfVoteEmails: [...authors],
|
|
1087
|
+
// 어느 규칙으로 위 집합을 만들었는가. 막힌 사람에게 **왜** 막혔는지 말해 주려면
|
|
1088
|
+
// 읽는 쪽이 이 값을 알아야 한다 — 모르면 "자기 표라서" 까지밖에 못 말한다.
|
|
1089
|
+
selfVoteMode: selfVote,
|
|
1067
1090
|
}
|
|
1068
1091
|
}
|
|
1069
1092
|
|
package/src/update.mjs
CHANGED
|
@@ -131,15 +131,33 @@ export const isStale = (rec, nowMs, interval = CHECK_INTERVAL_MS) =>
|
|
|
131
131
|
!rec || typeof rec.checkedAt !== 'number' || nowMs - rec.checkedAt >= interval
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* 사람에게 보여줄
|
|
134
|
+
* 사람에게 보여줄 안내. 알릴 것이 없으면 `null`.
|
|
135
135
|
*
|
|
136
136
|
* 🔴 부르는 쪽은 이것을 **stderr 로** 낸다. stdout 은 `--json` 이 쓰는 자리라,
|
|
137
137
|
* 여기에 사람이 읽는 글을 섞으면 그 출력을 파싱하는 쪽이 조용히 깨진다.
|
|
138
|
+
*
|
|
139
|
+
* 🔴 **두 줄을 다 낸다 (2026-09-12).** 이 PC 에 판이 두 군데 있기 때문이다 —
|
|
140
|
+
* `axmap` 명령(전역 설치)과 git 훅이 부르는 사본(`~/.axmap/app`). 서로 안
|
|
141
|
+
* 따라간다.
|
|
142
|
+
*
|
|
143
|
+
* 전에는 `npx … setup` 한 줄만 안내했다. 그런데 **`setup` 은 전역 설치를 전혀
|
|
144
|
+
* 건드리지 않는다** (`tools/setup.mjs` 에 `npm i` 가 0건이다 — 거기 있는
|
|
145
|
+
* `--commands global` 의 "global" 은 슬래시 명령을 어디 둘지이지 전역 설치가
|
|
146
|
+
* 아니다). 그래서 그 안내를 그대로 따르면 **사본만 새 판이 되고 `axmap` 명령은
|
|
147
|
+
* 옛 판으로 남는다.**
|
|
148
|
+
*
|
|
149
|
+
* 그건 이 명령이 1.2.4 에서 고친 문제(사본이 낡았는데 "최신입니다" 라고 한 것)를
|
|
150
|
+
* **안내가 스스로 만들어 내는 것**이다. 반쪽짜리 안내는 틀린 안내보다 낫지
|
|
151
|
+
* 않다 — 시킨 대로 했는데 안 고쳐지면 사람은 도구를 안 믿게 된다.
|
|
138
152
|
*/
|
|
139
153
|
export function noticeLine(current, latest) {
|
|
140
154
|
if (!current || !latest) return null
|
|
141
155
|
if (compareVersions(latest, current) <= 0) return null
|
|
142
|
-
return `새 버전이 있습니다: ${current} → ${latest}\n
|
|
156
|
+
return `새 버전이 있습니다: ${current} → ${latest}\n`
|
|
157
|
+
+ ` 받으려면: npm i -g ${PACKAGE_NAME}@latest\n`
|
|
158
|
+
+ ` npx -y ${PACKAGE_NAME}@latest setup\n`
|
|
159
|
+
+ ' 🔴 두 줄 다 필요합니다 — 앞은 axmap 명령을, 뒤는 git 훅이 부르는 사본을\n'
|
|
160
|
+
+ ' 올립니다. 서로 안 따라갑니다.'
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
/**
|