앱 위젯에서 구분선 리사이즈 안되는 문제 수정
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
|
"@capacitor/browser": "^7.0.5",
|
||||||
"@capacitor/core": "^7.0.0",
|
"@capacitor/core": "^7.0.0",
|
||||||
"@capacitor/haptics": "^7.0.0",
|
"@capacitor/haptics": "^7.0.0",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* Android 앱 내 업데이트 — /api/mobile-app/info 버전 비교 후 APK 다운로드
|
* Android 앱 내 업데이트 — /api/mobile-app/info 버전 비교 후 APK 다운로드
|
||||||
*/
|
*/
|
||||||
import { App } from '@capacitor/app';
|
import { App } from '@capacitor/app';
|
||||||
|
import { Browser } from '@capacitor/browser';
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { Preferences } from '@capacitor/preferences';
|
||||||
import {
|
import {
|
||||||
isRemoteVersionNewer,
|
isRemoteVersionNewer,
|
||||||
loadMobileAppReleaseInfo,
|
loadMobileAppReleaseInfo,
|
||||||
@@ -16,7 +18,8 @@ export interface AppUpdateCheckResult {
|
|||||||
release?: MobileAppReleaseInfoDto | null;
|
release?: MobileAppReleaseInfoDto | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const skipKey = (remote: string) => `gc_skip_update_${remote}`;
|
const skipSessionKey = (remote: string) => `gc_skip_update_${remote}`;
|
||||||
|
const pendingInstallKey = 'gc_pending_update_install';
|
||||||
|
|
||||||
export async function getCurrentAppVersion(): Promise<string> {
|
export async function getCurrentAppVersion(): Promise<string> {
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
@@ -30,10 +33,49 @@ export async function getCurrentAppVersion(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getPendingInstallVersion(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { value } = await Preferences.get({ key: pendingInstallKey });
|
||||||
|
return value?.trim() || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setPendingInstallVersion(remoteVersion: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Preferences.set({ key: pendingInstallKey, value: remoteVersion });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearPendingInstallVersion(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Preferences.remove({ key: pendingInstallKey });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 설치 완료 시 pending 플래그 제거 */
|
||||||
|
async function reconcilePendingInstall(currentVersion: string, remoteVersion?: string): Promise<void> {
|
||||||
|
const pending = await getPendingInstallVersion();
|
||||||
|
if (!pending) return;
|
||||||
|
if (remoteVersion && !isRemoteVersionNewer(currentVersion, remoteVersion)) {
|
||||||
|
await clearPendingInstallVersion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isRemoteVersionNewer(currentVersion, pending)) {
|
||||||
|
await clearPendingInstallVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function checkForAppUpdate(): Promise<AppUpdateCheckResult> {
|
export async function checkForAppUpdate(): Promise<AppUpdateCheckResult> {
|
||||||
const currentVersion = await getCurrentAppVersion();
|
const currentVersion = await getCurrentAppVersion();
|
||||||
const release = await loadMobileAppReleaseInfo();
|
const release = await loadMobileAppReleaseInfo();
|
||||||
const remoteVersion = release?.version?.trim();
|
const remoteVersion = release?.version?.trim();
|
||||||
|
await reconcilePendingInstall(currentVersion, remoteVersion);
|
||||||
const updateAvailable = Boolean(
|
const updateAvailable = Boolean(
|
||||||
release?.available
|
release?.available
|
||||||
&& remoteVersion
|
&& remoteVersion
|
||||||
@@ -49,14 +91,19 @@ export async function openApkDownload(release?: MobileAppReleaseInfoDto | null):
|
|||||||
if (!url) return false;
|
if (!url) return false;
|
||||||
|
|
||||||
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
|
if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android') {
|
||||||
const opened = window.open(url, '_blank', 'noopener,noreferrer');
|
try {
|
||||||
if (opened) return true;
|
await Browser.open({ url, presentationStyle: 'popover' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
/* Browser 플러그인 실패 시 anchor fallback */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.rel = 'noopener';
|
a.rel = 'noopener';
|
||||||
a.download = info?.fileName ?? 'goldenchart-android.apk';
|
a.download = info?.fileName ?? 'goldenchart-android.apk';
|
||||||
|
a.target = '_blank';
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
@@ -65,7 +112,7 @@ export async function openApkDownload(release?: MobileAppReleaseInfoDto | null):
|
|||||||
|
|
||||||
export function isUpdateDismissed(remoteVersion: string): boolean {
|
export function isUpdateDismissed(remoteVersion: string): boolean {
|
||||||
try {
|
try {
|
||||||
return sessionStorage.getItem(skipKey(remoteVersion)) === '1';
|
return sessionStorage.getItem(skipSessionKey(remoteVersion)) === '1';
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -73,13 +120,24 @@ export function isUpdateDismissed(remoteVersion: string): boolean {
|
|||||||
|
|
||||||
export function dismissUpdate(remoteVersion: string): void {
|
export function dismissUpdate(remoteVersion: string): void {
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(skipKey(remoteVersion), '1');
|
sessionStorage.setItem(skipSessionKey(remoteVersion), '1');
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 업데이트 확인 후 confirm → APK 다운로드. skipIfDismissed면 세션 내 거부 버전 생략 */
|
/** 다운로드 시작 후 같은 버전 재알림 방지 (설치 완료 전) */
|
||||||
|
export async function isUpdatePendingInstall(remoteVersion: string): Promise<boolean> {
|
||||||
|
const pending = await getPendingInstallVersion();
|
||||||
|
return pending === remoteVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shouldSkipUpdatePrompt(remoteVersion: string): Promise<boolean> {
|
||||||
|
if (isUpdateDismissed(remoteVersion)) return true;
|
||||||
|
return isUpdatePendingInstall(remoteVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 업데이트 확인 후 confirm → APK 다운로드. skipIfDismissed면 세션 거부·다운로드 대기 중 생략 */
|
||||||
export async function promptAppUpdate(options?: {
|
export async function promptAppUpdate(options?: {
|
||||||
skipIfDismissed?: boolean;
|
skipIfDismissed?: boolean;
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
@@ -92,7 +150,7 @@ export async function promptAppUpdate(options?: {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.skipIfDismissed && isUpdateDismissed(check.remoteVersion)) {
|
if (options?.skipIfDismissed && await shouldSkipUpdatePrompt(check.remoteVersion)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +165,12 @@ export async function promptAppUpdate(options?: {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setPendingInstallVersion(check.remoteVersion);
|
||||||
await openApkDownload(check.release);
|
await openApkDownload(check.release);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 설정 화면 — 수동 업데이트 확인 */
|
/** 설정 화면 — 수동 업데이트 확인 (pending 스킵 무시) */
|
||||||
export async function checkAndPromptAppUpdate(): Promise<AppUpdateCheckResult> {
|
export async function checkAndPromptAppUpdate(): Promise<AppUpdateCheckResult> {
|
||||||
const result = await checkForAppUpdate();
|
const result = await checkForAppUpdate();
|
||||||
if (!result.updateAvailable) {
|
if (!result.updateAvailable) {
|
||||||
@@ -122,7 +181,8 @@ export async function checkAndPromptAppUpdate(): Promise<AppUpdateCheckResult> {
|
|||||||
`새 버전 v${result.remoteVersion} 사용 가능.\n`
|
`새 버전 v${result.remoteVersion} 사용 가능.\n`
|
||||||
+ `현재 v${result.currentVersion}\n\nAPK를 다운로드하시겠습니까?`,
|
+ `현재 v${result.currentVersion}\n\nAPK를 다운로드하시겠습니까?`,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok && result.remoteVersion) {
|
||||||
|
await setPendingInstallVersion(result.remoteVersion);
|
||||||
await openApkDownload(result.release);
|
await openApkDownload(result.release);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -57,12 +57,11 @@ public class MobileAppReleaseService {
|
|||||||
Instant updated = Files.getLastModifiedTime(file).toInstant();
|
Instant updated = Files.getLastModifiedTime(file).toInstant();
|
||||||
String base = normalizeBase(publicBaseUrl);
|
String base = normalizeBase(publicBaseUrl);
|
||||||
String downloadUrl = base + "/api/mobile-app/download/android";
|
String downloadUrl = base + "/api/mobile-app/download/android";
|
||||||
|
String version = resolveApkVersion(file);
|
||||||
return MobileAppReleaseInfoDto.builder()
|
return MobileAppReleaseInfoDto.builder()
|
||||||
.available(true)
|
.available(true)
|
||||||
.fileName(file.getFileName().toString())
|
.fileName(file.getFileName().toString())
|
||||||
.version(configuredVersion != null && !configuredVersion.isBlank()
|
.version(version)
|
||||||
? configuredVersion
|
|
||||||
: DT_FMT.format(updated))
|
|
||||||
.sizeBytes(size)
|
.sizeBytes(size)
|
||||||
.updatedAt(DT_FMT.format(updated))
|
.updatedAt(DT_FMT.format(updated))
|
||||||
.downloadUrl(downloadUrl)
|
.downloadUrl(downloadUrl)
|
||||||
@@ -141,4 +140,29 @@ public class MobileAppReleaseService {
|
|||||||
}
|
}
|
||||||
return publicBaseUrl.replaceAll("/+$", "");
|
return publicBaseUrl.replaceAll("/+$", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** APK와 함께 기록된 .version sidecar → .env configuredVersion → 파일 수정 시각 */
|
||||||
|
private String resolveApkVersion(Path apkFile) {
|
||||||
|
String apkName = apkFile.getFileName().toString();
|
||||||
|
Path sidecar = apkFile.getParent().resolve(
|
||||||
|
apkName.replaceAll("(?i)\\.apk$", "") + ".version");
|
||||||
|
try {
|
||||||
|
if (Files.isRegularFile(sidecar)) {
|
||||||
|
String fromFile = Files.readString(sidecar).trim();
|
||||||
|
if (!fromFile.isBlank()) {
|
||||||
|
return fromFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[mobile-app] read version sidecar failed: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
if (configuredVersion != null && !configuredVersion.isBlank()) {
|
||||||
|
return configuredVersion.trim();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return DT_FMT.format(Files.getLastModifiedTime(apkFile).toInstant());
|
||||||
|
} catch (IOException e) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,10 +71,11 @@
|
|||||||
'<p class="muted" style="margin-top:12px">' + (info.version ? 'v' + info.version + ' · ' : '') +
|
'<p class="muted" style="margin-top:12px">' + (info.version ? 'v' + info.version + ' · ' : '') +
|
||||||
(info.sizeBytes ? (info.sizeBytes / 1048576).toFixed(1) + ' MB' : '') + '</p>' +
|
(info.sizeBytes ? (info.sizeBytes / 1048576).toFixed(1) + ' MB' : '') + '</p>' +
|
||||||
'<a class="btn" href="' + url + '" download>APK 다운로드</a>' +
|
'<a class="btn" href="' + url + '" download>APK 다운로드</a>' +
|
||||||
'<ol style="margin-top:20px"><li>다운로드 후 APK 파일을 열어 설치</li>' +
|
'<ol style="margin-top:20px"><li>다운로드 후 APK 파일을 열어 설치를 <strong>완료</strong>하세요</li>' +
|
||||||
'<li>「알 수 없는 출처」 허용 필요 시 설정에서 활성화</li>' +
|
'<li>「알 수 없는 출처」 허용 필요 시 설정에서 활성화</li>' +
|
||||||
'<li>앱은 서버(exdev.co.kr)에 자동 연결됩니다</li>' +
|
'<li>패키지: <code>com.goldenchart.app</code> (다른 GoldenChart 앱과 구분)</li>' +
|
||||||
'<li>새 버전은 앱 시작·설정 → 업데이트 확인</li></ol>';
|
'<li>「Unable to load script」 오류 시 예전 앱을 삭제 후 재설치</li>' +
|
||||||
|
'<li>앱은 서버(exdev.co.kr)에 자동 연결됩니다</li></ol>';
|
||||||
QRCode.toCanvas(document.getElementById('qr'), url, { width: 200, margin: 1 });
|
QRCode.toCanvas(document.getElementById('qr'), url, { width: 200, margin: 1 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.innerHTML = '<p class="err">앱 정보를 불러오지 못했습니다.</p>';
|
el.innerHTML = '<p class="err">앱 정보를 불러오지 못했습니다.</p>';
|
||||||
|
|||||||
@@ -37,9 +37,27 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
nativeShell = false,
|
nativeShell = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
|
||||||
|
const bodyWrapRef = useRef<HTMLDivElement>(null);
|
||||||
const bodyRef = useRef<HTMLDivElement>(null);
|
const bodyRef = useRef<HTMLDivElement>(null);
|
||||||
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
|
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
const resolveBodySize = useCallback(() => {
|
||||||
|
const wrap = bodyWrapRef.current;
|
||||||
|
const body = bodyRef.current;
|
||||||
|
const width = wrap?.clientWidth ?? body?.clientWidth ?? bodySize.width;
|
||||||
|
const height = wrap?.clientHeight ?? body?.clientHeight ?? bodySize.height;
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
if (nativeShell) {
|
||||||
|
return {
|
||||||
|
width: width > 0 ? width : instance.width,
|
||||||
|
height: height > 0 ? height : instance.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { width, height };
|
||||||
|
}, [bodySize.height, bodySize.width, instance.height, instance.width, nativeShell]);
|
||||||
|
|
||||||
const rowFr = instance.rowFr?.length === instance.rows
|
const rowFr = instance.rowFr?.length === instance.rows
|
||||||
? instance.rowFr
|
? instance.rowFr
|
||||||
: defaultGridFr(instance.rows);
|
: defaultGridFr(instance.rows);
|
||||||
@@ -92,15 +110,17 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
onUpdateGridFr(next, colFrRef.current);
|
onUpdateGridFr(next, colFrRef.current);
|
||||||
}, [onUpdateGridFr]);
|
}, [onUpdateGridFr]);
|
||||||
|
|
||||||
|
const notifyLayoutSync = useCallback(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { onColSplitPointerDown, onRowSplitPointerDown } = useFloatingWidgetGridSplit({
|
const { onColSplitPointerDown, onRowSplitPointerDown } = useFloatingWidgetGridSplit({
|
||||||
onColFrChange: handleColFrChange,
|
onColFrChange: handleColFrChange,
|
||||||
onRowFrChange: handleRowFrChange,
|
onRowFrChange: handleRowFrChange,
|
||||||
getColFr: () => colFrRef.current,
|
getColFr: () => colFrRef.current,
|
||||||
getRowFr: () => rowFrRef.current,
|
getRowFr: () => rowFrRef.current,
|
||||||
getBodySize: () => ({
|
getBodySize: resolveBodySize,
|
||||||
width: bodyRef.current?.clientWidth ?? bodySize.width,
|
onSplitEnd: notifyLayoutSync,
|
||||||
height: bodyRef.current?.clientHeight ?? bodySize.height,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
|
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
|
||||||
@@ -119,24 +139,30 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
|
|
||||||
/* 창·그리드 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
|
/* 창·그리드 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = bodyRef.current;
|
const el = bodyWrapRef.current ?? bodyRef.current;
|
||||||
if (!el) return undefined;
|
if (!el) return undefined;
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
const scheduleLayoutSync = () => {
|
const scheduleLayoutSync = () => {
|
||||||
setBodySize({ width: el.clientWidth, height: el.clientHeight });
|
const wrap = bodyWrapRef.current;
|
||||||
|
const body = bodyRef.current;
|
||||||
|
setBodySize({
|
||||||
|
width: wrap?.clientWidth ?? body?.clientWidth ?? 0,
|
||||||
|
height: wrap?.clientHeight ?? body?.clientHeight ?? 0,
|
||||||
|
});
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(notifyLayoutSync, 80);
|
||||||
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
|
|
||||||
}, 80);
|
|
||||||
};
|
};
|
||||||
scheduleLayoutSync();
|
scheduleLayoutSync();
|
||||||
const ro = new ResizeObserver(() => scheduleLayoutSync());
|
const ro = new ResizeObserver(() => scheduleLayoutSync());
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
|
if (bodyRef.current && bodyRef.current !== el) {
|
||||||
|
ro.observe(bodyRef.current);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, [instance.rows, instance.cols]);
|
}, [instance.rows, instance.cols, notifyLayoutSync]);
|
||||||
|
|
||||||
const colSplitPositions = useMemo(
|
const colSplitPositions = useMemo(
|
||||||
() => gridSplitPositions(colFr, bodySize.width, 1),
|
() => gridSplitPositions(colFr, bodySize.width, 1),
|
||||||
@@ -193,7 +219,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="fw-window-body-wrap">
|
<div ref={bodyWrapRef} className="fw-window-body-wrap">
|
||||||
<div
|
<div
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
className="fw-window-body"
|
className="fw-window-body"
|
||||||
@@ -221,6 +247,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
key={`col-${index}`}
|
key={`col-${index}`}
|
||||||
className="fw-grid-splitter fw-grid-splitter--v"
|
className="fw-grid-splitter fw-grid-splitter--v"
|
||||||
style={{ left }}
|
style={{ left }}
|
||||||
|
data-no-chart-pan="true"
|
||||||
role="separator"
|
role="separator"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-label={`열 ${index + 1}·${index + 2} 경계 조절`}
|
aria-label={`열 ${index + 1}·${index + 2} 경계 조절`}
|
||||||
@@ -232,6 +259,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
|
|||||||
key={`row-${index}`}
|
key={`row-${index}`}
|
||||||
className="fw-grid-splitter fw-grid-splitter--h"
|
className="fw-grid-splitter fw-grid-splitter--h"
|
||||||
style={{ top }}
|
style={{ top }}
|
||||||
|
data-no-chart-pan="true"
|
||||||
role="separator"
|
role="separator"
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
aria-label={`행 ${index + 1}·${index + 2} 경계 조절`}
|
aria-label={`행 ${index + 1}·${index + 2} 경계 조절`}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
FLOATING_WIDGET_CELL_MIN_W,
|
FLOATING_WIDGET_CELL_MIN_W,
|
||||||
FLOATING_WIDGET_GRID_GAP,
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
} from '../types/floatingWidget';
|
} from '../types/floatingWidget';
|
||||||
import { bindWindowDrag, isPrimaryPointerButton } from '../utils/pointerDrag';
|
import { isPrimaryPointerButton } from '../utils/pointerDrag';
|
||||||
|
|
||||||
type SplitAxis = 'col' | 'row';
|
type SplitAxis = 'col' | 'row';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ interface Options {
|
|||||||
getColFr: () => number[];
|
getColFr: () => number[];
|
||||||
getRowFr: () => number[];
|
getRowFr: () => number[];
|
||||||
getBodySize: () => { width: number; height: number };
|
getBodySize: () => { width: number; height: number };
|
||||||
|
onSplitEnd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFloatingWidgetGridSplit({
|
export function useFloatingWidgetGridSplit({
|
||||||
@@ -23,18 +24,12 @@ export function useFloatingWidgetGridSplit({
|
|||||||
getColFr,
|
getColFr,
|
||||||
getRowFr,
|
getRowFr,
|
||||||
getBodySize,
|
getBodySize,
|
||||||
|
onSplitEnd,
|
||||||
}: Options) {
|
}: Options) {
|
||||||
const unbindDragRef = useRef<(() => void) | null>(null);
|
const activeDragRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
const endDrag = useCallback(() => {
|
|
||||||
unbindDragRef.current?.();
|
|
||||||
unbindDragRef.current = null;
|
|
||||||
document.body.style.cursor = '';
|
|
||||||
document.body.style.userSelect = '';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
unbindDragRef.current?.();
|
activeDragRef.current?.();
|
||||||
document.body.style.cursor = '';
|
document.body.style.cursor = '';
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = '';
|
||||||
}, []);
|
}, []);
|
||||||
@@ -60,43 +55,67 @@ export function useFloatingWidgetGridSplit({
|
|||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const startColFr = [...getColFr()];
|
const startColFr = [...getColFr()];
|
||||||
const startRowFr = [...getRowFr()];
|
const startRowFr = [...getRowFr()];
|
||||||
const startSize = getBodySize();
|
|
||||||
|
|
||||||
document.body.style.cursor = axis === 'col' ? 'col-resize' : 'row-resize';
|
document.body.style.cursor = axis === 'col' ? 'col-resize' : 'row-resize';
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
unbindDragRef.current?.();
|
activeDragRef.current?.();
|
||||||
unbindDragRef.current = bindWindowDrag(
|
|
||||||
(xy) => {
|
const onMove = (ev: PointerEvent) => {
|
||||||
if (axis === 'col') {
|
const size = getBodySize();
|
||||||
const delta = xy.x - startX;
|
if (axis === 'col') {
|
||||||
onColFrChange(adjustAdjacentGridFr(
|
if (size.width <= 0) return;
|
||||||
startColFr,
|
const delta = ev.clientX - startX;
|
||||||
index,
|
onColFrChange(adjustAdjacentGridFr(
|
||||||
delta,
|
startColFr,
|
||||||
startSize.width,
|
|
||||||
FLOATING_WIDGET_GRID_GAP,
|
|
||||||
FLOATING_WIDGET_CELL_MIN_W,
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = xy.y - startY;
|
|
||||||
onRowFrChange(adjustAdjacentGridFr(
|
|
||||||
startRowFr,
|
|
||||||
index,
|
index,
|
||||||
delta,
|
delta,
|
||||||
startSize.height,
|
size.width,
|
||||||
FLOATING_WIDGET_GRID_GAP,
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
FLOATING_WIDGET_CELL_MIN_H,
|
FLOATING_WIDGET_CELL_MIN_W,
|
||||||
));
|
));
|
||||||
},
|
return;
|
||||||
() => {
|
}
|
||||||
handle.classList.remove('fw-grid-splitter--active');
|
if (size.height <= 0) return;
|
||||||
endDrag();
|
const delta = ev.clientY - startY;
|
||||||
},
|
onRowFrChange(adjustAdjacentGridFr(
|
||||||
{ preventTouchScroll: true },
|
startRowFr,
|
||||||
);
|
index,
|
||||||
}, [endDrag, getBodySize, getColFr, getRowFr, onColFrChange, onRowFrChange]);
|
delta,
|
||||||
|
size.height,
|
||||||
|
FLOATING_WIDGET_GRID_GAP,
|
||||||
|
FLOATING_WIDGET_CELL_MIN_H,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUp = (ev: PointerEvent) => {
|
||||||
|
handle.classList.remove('fw-grid-splitter--active');
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
try {
|
||||||
|
if (handle.hasPointerCapture(ev.pointerId)) {
|
||||||
|
handle.releasePointerCapture(ev.pointerId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
document.removeEventListener('pointermove', onMove, true);
|
||||||
|
document.removeEventListener('pointerup', onUp, true);
|
||||||
|
document.removeEventListener('pointercancel', onUp, true);
|
||||||
|
activeDragRef.current = null;
|
||||||
|
onSplitEnd?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('pointermove', onMove, { capture: true });
|
||||||
|
document.addEventListener('pointerup', onUp, { capture: true });
|
||||||
|
document.addEventListener('pointercancel', onUp, { capture: true });
|
||||||
|
|
||||||
|
activeDragRef.current = () => {
|
||||||
|
document.removeEventListener('pointermove', onMove, true);
|
||||||
|
document.removeEventListener('pointerup', onUp, true);
|
||||||
|
document.removeEventListener('pointercancel', onUp, true);
|
||||||
|
};
|
||||||
|
}, [getBodySize, getColFr, getRowFr, onColFrChange, onRowFrChange, onSplitEnd]);
|
||||||
|
|
||||||
const onColSplitPointerDown = useCallback(
|
const onColSplitPointerDown = useCallback(
|
||||||
(index: number) => (e: React.PointerEvent) => startSplit(e, 'col', index),
|
(index: number) => (e: React.PointerEvent) => startSplit(e, 'col', index),
|
||||||
|
|||||||
@@ -648,8 +648,22 @@ html.fw-native-widget #root {
|
|||||||
.fw-window--native .fw-window-body-wrap {
|
.fw-window--native .fw-window-body-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fw-window--native .fw-window-body {
|
.fw-window--native .fw-window-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw-window--native .fw-grid-split-layer {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fw-window--native .fw-grid-splitter {
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|||||||
Generated
+10
@@ -18,6 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
|
"@capacitor/browser": "^7.0.5",
|
||||||
"@capacitor/core": "^7.0.0",
|
"@capacitor/core": "^7.0.0",
|
||||||
"@capacitor/haptics": "^7.0.0",
|
"@capacitor/haptics": "^7.0.0",
|
||||||
"@capacitor/ios": "^7.0.0",
|
"@capacitor/ios": "^7.0.0",
|
||||||
@@ -421,6 +422,15 @@
|
|||||||
"@capacitor/core": ">=7.0.0"
|
"@capacitor/core": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor/browser": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-YF/kC1m1e6c81X/agtAnYItVlqiQ/nXLG+Q3QXNDuiMoRynqa3Guu8ZaV9kWvqQcHoE2w18xz7WGpPOuh+R1fA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor/cli": {
|
"node_modules/@capacitor/cli": {
|
||||||
"version": "7.6.5",
|
"version": "7.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.5.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ sign_release_apk() {
|
|||||||
--ks-pass pass:goldenchart --key-pass pass:goldenchart "$signed"
|
--ks-pass pass:goldenchart --key-pass pass:goldenchart "$signed"
|
||||||
mkdir -p "$(dirname "$LOCAL_APK")"
|
mkdir -p "$(dirname "$LOCAL_APK")"
|
||||||
cp "$signed" "$LOCAL_APK"
|
cp "$signed" "$LOCAL_APK"
|
||||||
|
|
||||||
|
VERSION_NAME="$(grep -E 'versionName[[:space:]]+"' "$ROOT/app/android/app/build.gradle" | head -1 | sed -E 's/.*versionName[[:space:]]+"([^"]+)".*/\1/' || true)"
|
||||||
|
if [[ -n "$VERSION_NAME" ]]; then
|
||||||
|
printf '%s\n' "$VERSION_NAME" > "${LOCAL_APK%.apk}.version"
|
||||||
|
log "버전 sidecar: ${LOCAL_APK%.apk}.version → $VERSION_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
log "서명 완료: $LOCAL_APK"
|
log "서명 완료: $LOCAL_APK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user