import { useEffect, useMemo, useRef, useState } from 'react'
import { faker } from '@faker-js/faker'

type Person = {
	id: number
	// userId: string
	firstName: string
	lastName: string
	age: number
}

const range = (len: number) => {
	const arr: number[] = []
	for (let i = 0; i < len; i++) {
		arr.push(i)
	}
	return arr
}

const newPerson = (num: number): Person => {
	return {
		id: num,
		// userId: faker.string.uuid(),
		firstName: faker.person.firstName(),
		lastName: faker.person.lastName(),
		age: faker.number.int(40),
	}
}

const ROW_HEIGHT = 35

const rows = range(2000).map((index): Person => newPerson(index))
const rowCount = rows.length

export const VirtualScroll = () => {
	const gridRef = useRef<HTMLDivElement>(null)

	const [scrollTop, setScrollTop] = useState(0)
	const [clientHeight, setClientHeight] = useState(0)

	const handleScroll = ({ currentTarget: { scrollTop } }: React.UIEvent<HTMLDivElement>) => setScrollTop(scrollTop)

	useEffect(() => {
		if (!gridRef.current) return
		const handleResize = () => {
			if (!gridRef.current) return
			setClientHeight(gridRef.current.clientHeight)
		}

		const { scrollTop, clientHeight } = gridRef.current

		setScrollTop(scrollTop)
		setClientHeight(clientHeight)
		window.addEventListener('resize', handleResize)

		return () => {
			window.removeEventListener('resize', handleResize)
		}
	}, [])

	const { startIndex, endIndex } = useMemo(() => {
		const viewportHeight = clientHeight
		const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT))
		const visibleRowCount = Math.min(rowCount - startIndex, Math.ceil(viewportHeight / ROW_HEIGHT))
		const endIndex = startIndex + visibleRowCount

		return { startIndex, endIndex }
	}, [clientHeight, scrollTop])

	const visibleRows = rows.slice(startIndex, endIndex)

	return (
		<div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
			{/* <div style={{ display: 'flex', gap: '1em' }}>
				<span>scrollTop: {scrollTop.toLocaleString()}px</span>
				<span>clientHeight: {clientHeight.toLocaleString()}px</span>
				<span>startNode: {startIndex.toLocaleString()}</span>
				<span>visibleNodesCount: {endIndex - startIndex}</span>
			</div> */}
			<div
				ref={gridRef}
				style={{
					display: 'grid',
					blockSize: 350,
					flexGrow: 1,
					gridTemplateRows: `repeat(${rows.length}, 35px)`,
					overflow: 'auto',
					border: '1px solid gray',
				}}
				onScroll={handleScroll}
			>
				{visibleRows.map((item, i: number) => (
					<div
						key={item.id}
						style={{
							gridRowStart: startIndex + i + 1,
							display: 'flex',
							gap: '1em',
							height: ROW_HEIGHT,
							borderBottom: '1px solid gray',
						}}
					>
						<span>{i + 1}</span>
						<span>{item.id}</span>
						{/* <span>{item.userId}</span> */}
						<span>{item.firstName}</span>
						<span>{item.lastName}</span>
						<span>{item.age}</span>
					</div>
				))}
			</div>
		</div>
	)
}

브라우저 resizing 까지는 대응했지만, debounce 미적용이라...

다양한 루트로 얻은 소스를 짬뽕한거라 일관성은 없지만 대략 사용방법을 터득하기에는 괜찮을 것 같아서 정리해봄

최종 채택은 2번으로 했지만 나머지도 기록 삼아 남김

 

1. 클라이언트 - heic2any: webp 바로 전환은 안되서 png 거쳐서 변환. 갤럭시 울트라 200M 사진은 처리불가

'use client'

import { useState } from 'react'

export function FileUpload() {
	const [file, setFile] = useState<any>()

	// 파일 선택 시 처리
	const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
		const file = event.target.files?.[0]
		if (!file) return
		
		const isHeic =
			file.type === 'image/heic' ||
			file.name.toLowerCase().endsWith('.heic') ||
			file.name.toLowerCase().endsWith('.heif')

		if (isHeic) {
			try {
				const heic2any = (await import('heic2any')).default
				// 1. HEIC/HEIF 파일을 PNG Blob으로 변환
				const pngBlob = await heic2any({
					blob: file,
					toType: 'image/png',
				})

				// 2. PNG Blob을 File 객체로 변환 및 WebP로 Canvas 변환
				const convertedFile = await convertToWebP(pngBlob, file.name)
				setFile(convertedFile)
			} catch (error) {
				console.error('### HEIC 변환 실패:', error)
				alert('이미지 변환에 실패했습니다. 다른 이미지를 시도해주세요.')
			}
		} else {
			setFile(file)
		}
	}

	// Blob을 WebP 포맷 File 객체로 변환하는 함수
	const convertToWebP = (blob: Blob | Blob[], originalName: string): Promise<File> => {
		return new Promise((resolve, reject) => {
			const img = new Image()
			const url = URL.createObjectURL(Array.isArray(blob) ? blob[0] : blob)

			img.onload = () => {
				URL.revokeObjectURL(url)
				const { width, height } = imageResize(img.width, img.height)

				// Canvas 생성
				const canvas = document.createElement('canvas')
				canvas.width = width
				canvas.height = height
				const ctx = canvas.getContext('2d')
				if (!ctx) throw new Error('Canvas context를 생성할 수 없습니다.')
				ctx.drawImage(img, 0, 0)
				// ctx.drawImage(img, 0, 0, targetWidth, targetHeight)

				canvas.toBlob(
					(webpBlob) => {
						if (!webpBlob) throw new Error('WebP 변환에 실패했습니다.')
						const newName = originalName.replace(/\.(heic|heif)$/i, '.webp')
						const webpFile = new File([webpBlob], newName, { type: 'image/webp' })
						resolve(webpFile)
					},
					'image/webp',
					0.8,
				)
			}
			img.onerror = (err) => reject(err)
			img.src = url
		})
	}

	// 폼 전송
	const handleSubmit = async (e) => {
		e.preventDefault()
		if (!file) return alert('파일을 선택해주세요.')

		const formData = new FormData()
		formData.append('image', file)
	}

	return (
		<form onSubmit={handleSubmit}>
			<input type="file" accept="image/*" onChange={handleFileChange} />
			<button type="submit">전송</button>
		</form>
	)
}

 

2. 클라이언트 - heic-decode: heic2any가 100%라면 60% 정도의 속도로 빠르게 처리하나 빌드 후 라이브러리 용량이 다소 나감. 갤럭시 울트라 200M 사진은 처리불가

'use client'

import { useState } from 'react'

export function FileUpload() {
	const [loading, setLoading] = useState(false)

	const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
		const file = event.target.files?.[0]
		if (!file) return

		setLoading(true)
		let fileToSend: File | Blob = file

		const isHeic = file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif')

		if (isHeic) {
			try {
				const arrayBuffer = await file.arrayBuffer()

				// 1. HEIC 내부의 원본 픽셀 데이터(RGBA) 추출
				const decode = (await import('heic-decode')).default // 399.78 KB => 910.64 KB
				const { width, height, data } = await decode({ buffer: new Uint8Array(arrayBuffer) })
				const { width: targetWidth, height: targetHeight } = imageResize(width, height)

				// 3. 임시 오프스크린 캔버스 생성하여 리사이징 처리
				const canvas = document.createElement('canvas')
				canvas.width = targetWidth
				canvas.height = targetHeight
				const ctx = canvas.getContext('2d')

				if (!ctx) throw new Error('Canvas context를 생성할 수 없습니다.')

				// 원본 거대 데이터를 임시 ImageData 객체로 만듦
				const originalImageData = new ImageData(new Uint8ClampedArray(data), width, height)

				if (width === targetWidth && height === targetHeight) {
					// 크기가 동일하면 그대로 그림
					ctx.putImageData(originalImageData, 0, 0)
				} else {
					// 임시 캔버스를 거쳐 브라우저 하드웨어 가속을 이용해 부드럽게 축소(Downscale)합니다.
					const tempCanvas = document.createElement('canvas')
					tempCanvas.width = width
					tempCanvas.height = height
					tempCanvas.getContext('2d')?.putImageData(originalImageData, 0, 0)

					// 최종 캔버스에 축소해서 그리기
					ctx.drawImage(tempCanvas, 0, 0, targetWidth, targetHeight)
				}

				// 4. 🔥 [수정] 캔버스 내용을 WebP Blob으로 변환
				const webpBlob = await new Promise<Blob | null>((resolve) => {
					canvas.toBlob((blob) => resolve(blob), 'image/webp', 0.8)
				})

				if (!webpBlob) throw new Error('WebP 변환에 실패했습니다.')

				// 5. 🔥 [수정] 서버 전송용 File 객체 확장자를 .webp로 재포장
				const newFileName = file.name.replace(/\.(heic|heif)$/i, '.webp')
				fileToSend = new File([webpBlob], newFileName, { type: 'image/webp' })
			} catch (error) {
				console.error('브라우저 내 초고해상도 변환 중 에러 발생:', error)
				alert('이미지가 너무 커서 변환에 실패했습니다. 일반 JPG/PNG를 사용해주세요.')
				setLoading(false)
				return
			}
		}

		// 6. Next.js Route Handler로 FormData 전송
		try {
			const formData = new FormData()
			formData.append('file', fileToSend)
		} catch (err) {
			console.error(err)
		} finally {
			setLoading(false)
		}
	}
	return (
		<>
			<div>
				<input type="file" accept=".heic,.heif" onChange={handleFileChange} disabled={loading} />
				{loading && <p>이미지 최적화 및 변환 중... 잠시만 기다려주세요 (화면을 끄지 마세요)</p>}
			</div>
		</>
	)
}

 

3. 클라이언트 - @imagemagick/magick-wasm: 갤럭시 울트라 200M 사진도 처리가 가능하나 속도가 압도적으로 느리다.

'use client'
import NextImage from 'next/image'
import { useState } from 'react'

async function initMagick() {
    const { initializeImageMagick } = await import('@imagemagick/magick-wasm')
	const response = await fetch('/magick.wasm') // public 경로. 해당 파일은 \node_modules\@imagemagick\magick-wasm\dist 안에 있음
	const wasmBuffer = await response.arrayBuffer()
	await initializeImageMagick(wasmBuffer)
}
initMagick()

export function FileUpload() {
	const [outputUrl, setOutputUrl] = useState({ src: '', width: 0, height: 0 })

	const handleConvert = async (event: React.ChangeEvent<HTMLInputElement>) => {
		const file = event.target.files?.[0]
		if (!file) return
		const { ImageMagick } = await import('@imagemagick/magick-wasm')

		// 2. Read file as Uint8Array
		const arrayBuffer = await file.arrayBuffer()
		const data = new Uint8Array(arrayBuffer)

		// 3. Perform transformation
		ImageMagick.read(data, (image) => {
			const { width, height } = imageResize(image.width, image.height)
			image.resize(width, height)
			image.format = 'WEBP'

			// 4. Write back to a Blob for display
			image.write((outputData) => {
				const url = window.URL.createObjectURL(new Blob([new Uint8Array(outputData).buffer], { type: 'image/webp' }))
				setOutputUrl({ src: url, width, height })
			})
		})
	}

	return (
		<div>
			<input type="file" onChange={handleConvert} />
			{outputUrl.src && <NextImage {...outputUrl} alt="Converted" />}
		</div>
	)
}

function imageResize(width: number, height: number) {
	const MAX_WIDTH = 4000
	const MAX_HEIGHT = 4000
	let targetWidth = width
	let targetHeight = height

	if (width > MAX_WIDTH || height > MAX_HEIGHT) {
		if (width > height) {
			targetWidth = MAX_WIDTH
			targetHeight = Math.round((height * MAX_WIDTH) / width)
		} else {
			targetHeight = MAX_HEIGHT
			targetWidth = Math.round((width * MAX_HEIGHT) / height)
		}
	}
	return { width: targetWidth, height: targetHeight }
}

 

4. 서버 - heic-convert, sharp: 갤럭시 울트라 200M 사진은 처리불가

import { NextRequest, NextResponse } from 'next/server';
import convert from 'heic-convert'
import sharp from 'sharp'

async function handler(req: NextRequest) {


	// ...전략
	
		const formdata = await req.formData();

		const newFormData = new FormData()

		for (const [key, value] of formdata.entries()) {
			if (!value) {
				newFormData.append(key, '')
			} else if (value instanceof File === false) {
				newFormData.append(key, value)
			} else {
				const file = value
				const newFileName = file.name.split('.')
				const ext = newFileName.pop()

				if (!ext || ['heic', 'heif'].includes(ext.toLowerCase()) === false) {
					newFormData.append(key, value)
				} else {
					const outputBuffer = await convert({
						buffer: new Uint8Array(await file.arrayBuffer()) as unknown as ArrayBuffer,
						format: 'PNG',
					})
					// 2. 변환된 버퍼를 sharp로 처리 (리사이징 등)
					const webpBuffer = await sharp(outputBuffer).webp({ quality: 80 }).toBuffer()

					// 3. Web API의 File 객체 형태로 만들어 FormData에 추가
					const finalFile = new File([webpBuffer as any], `${newFileName}.webp`, { type: 'image/webp' })
					newFormData.append(key, finalFile)
				}
			}
		}

// ...후략		
}

export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const OPTIONS = handler;

 

공통함수

export function imageResize(width: number, height: number) {
	const MAX_WIDTH = 4000
	const MAX_HEIGHT = 4000
	let targetWidth = width
	let targetHeight = height

	if (width > MAX_WIDTH || height > MAX_HEIGHT) {
		if (width > height) {
			targetWidth = MAX_WIDTH
			targetHeight = Math.round((height * MAX_WIDTH) / width)
		} else {
			targetHeight = MAX_HEIGHT
			targetWidth = Math.round((width * MAX_HEIGHT) / height)
		}
	}
	return { width: targetWidth, height: targetHeight }
}

 

 

 백업 파일 확인

  • RESTORE HEADERONLY FROM DISK='C:\Backup\A.bak';
    → 백업된 SQL Server 버전, 날짜, 종류(FULL/DIFF/LOG) 확인
  • RESTORE FILELISTONLY FROM DISK='C:\Backup\A.bak';
    → 데이터/로그 파일 논리명 확인

 

📌 기본 FULL 백업 복원

sqlcmd -S 서버명\인스턴스명 -E -Q " RESTORE DATABASE B FROM DISK = 'C:\Backup\A.bak' WITH MOVE 'A_Data' TO 'C:\Data\B.mdf', MOVE 'A_Log' TO 'C:\Data\B_log.ldf', REPLACE, STATS=10;"
  • -S : 서버명\인스턴스명 (로컬 기본 인스턴스면 -S . 가능)
  • -E : Windows 인증 (SQL 로그인 쓰려면 -U 사용자 -P 비밀번호)
  • MOVE : 논리 파일명을 새 물리 경로로 지정
  • REPLACE : 기존 DB 덮어쓰기
  • STATS=10 : 진행률 10% 단위 출력

📌 DIFF 백업 복원 (FULL 복원 후)

sqlcmd -S 서버명\인스턴스명 -E -Q " RESTORE DATABASE B FROM DISK = 'C:\Backup\A_diff.bak' WITH NORECOVERY, STATS=10;"
  • NORECOVERY : 이후 로그 복원을 위해 DB를 복원 모드로 유지

📌 LOG 백업 복원 (DIFF 후 이어서)

sqlcmd -S 서버명\인스턴스명 -E -Q " RESTORE LOG B FROM DISK = 'C:\Backup\A_log.trn' WITH RECOVERY, STATS=10;"
  • RECOVERY : 마지막 로그 복원 후 DB를 ONLINE으로 전환

📌 복원 후 무결성 검사

sqlcmd -S 서버명\인스턴스명 -E -Q " DBCC CHECKDB ('B') WITH NO_INFOMSGS, ALL_ERRORMSGS;"

📌 자주 쓰는 팁

  • 헤더 확인 : RESTORE HEADERONLY FROM DISK='A.bak'
  • 파일 목록 확인 : RESTORE FILELISTONLY FROM DISK='A.bak'
  • 복원 진행률 확인 : 다른 세션에서
SELECT percent_complete, estimated_completion_time FROM sys.dm_exec_requests WHERE command = 'RESTORE DATABASE';

 

SSMS 에서 잘 안될때는 sqlcmd로 하면 잘 된다.

 

 

 

추가 점검/최적화 (선택)

  • sp_updatestats 실행 → 통계 갱신
  • 인덱스 재구성/재빌드 → 성능 최적화
  • 새 FULL 백업 생성 → 이후 백업 체인 새로 시작

🔧 sp_updatestats 실행

USE DB명;
EXEC sp_updatestats;


🔧 인덱스 재구성 (Reorganize)

  • 특징: 조각난 인덱스를 정리해서 페이지를 압축하고, 구조를 최적화
  • 장점: 빠르고 온라인으로 실행 가능 (대부분의 경우 DB 사용 중에도 가능)
  • 예시:

USE B; ALTER INDEX ALL ON dbo.테이블명 REORGANIZE;

🔧 인덱스 재빌드 (Rebuild)

  • 특징: 인덱스를 완전히 새로 만드는 작업
  • 장점: 조각난 정도가 심할 때 가장 효과적
  • 단점: 시간이 오래 걸리고, 기본적으로 오프라인 작업 (Enterprise Edition은 ONLINE 옵션 가능)
  • 예시:

USE B; ALTER INDEX ALL ON dbo.테이블명 REBUILD WITH (FILLFACTOR = 90, ONLINE = ON);

  • FILLFACTOR : 페이지에 여유 공간을 남겨둬서 이후 삽입 성능 개선
  • ONLINE = ON : Enterprise Edition에서만 지원, 작업 중에도 테이블 사용 가능

🔧 전체 DB 인덱스 최적화

  • 모든 테이블에 대해 인덱스를 재구성/재빌드하려면:

USE B; EXEC sp_MSforeachtable 'ALTER INDEX ALL ON ? REBUILD';

  • 또는

USE B; EXEC sp_MSforeachtable 'ALTER INDEX ALL ON ? REORGANIZE';

📌 선택 기준

  • 조각난 정도가 10~30% → REORGANIZE
  • 조각난 정도가 30% 이상 → REBUILD
  • 조각난 정도 확인:
SELECT
    dbschemas.[name] as 'Schema',
    dbtables.[name] as 'Table',
    dbindexes.[name] as 'Index',
    indexstats.avg_fragmentation_in_percent,
    indexstats.page_count
FROM sys.dm_db_index_physical_stats (DB_ID('B'), NULL, NULL, NULL, 'LIMITED') AS indexstats
    INNER JOIN sys.tables dbtables on dbtables.[object_id] = indexstats.[object_id]
    INNER JOIN sys.schemas dbschemas on dbtables.[schema_id] = dbschemas.[schema_id]
    INNER JOIN sys.indexes AS dbindexes ON dbindexes.[object_id] = indexstats.[object_id]
                                         AND indexstats.index_id = dbindexes.index_id
WHERE indexstats.database_id = DB_ID('B');

 

📌 자동 인덱스 유지보수 스크립트 (Enterprise Edition용)

USE B;
GO

DECLARE @TableName NVARCHAR(128);
DECLARE @IndexName NVARCHAR(128);
DECLARE @SQL NVARCHAR(MAX);

-- 인덱스 조각난 정도 확인 후 자동 처리
DECLARE cur CURSOR FOR
SELECT
    t.name AS TableName,
    i.name AS IndexName,
    ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
JOIN sys.tables t ON ips.object_id = t.object_id
WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
  AND ips.page_count > 100; -- 작은 인덱스는 제외

OPEN cur;
FETCH NEXT FROM cur INTO @TableName, @IndexName, @SQL;

WHILE @@FETCH_STATUS = 0
BEGIN
    IF @SQL BETWEEN 10 AND 30
        SET @SQL = 'ALTER INDEX [' + @IndexName + '] ON [' + @TableName + '] REORGANIZE;';
    ELSE IF @SQL > 30
        SET @SQL = 'ALTER INDEX [' + @IndexName + '] ON [' + @TableName + '] REBUILD WITH (ONLINE = ON);';
    ELSE
        SET @SQL = NULL;

    IF @SQL IS NOT NULL
    BEGIN
        PRINT '실행: ' + @SQL;
        EXEC (@SQL);
    END

    FETCH NEXT FROM cur INTO @TableName, @IndexName, @SQL;
END

CLOSE cur;
DEALLOCATE cur;
GO

📌 설명

  • sys.dm_db_index_physical_stats: 인덱스 조각난 정도 확인
  • 조건:
  • page_count > 100: 너무 작은 인덱스는 제외 (효과 없음)
  • ONLINE 옵션: Enterprise Edition에서만 지원, Standard Edition은 제거해야 함

📌 실행 팁

  • 복원 직후나 주기적인 유지보수 작업에 사용
  • 실행 전 반드시 DB를 사용하지 않는 시간대에 돌리는 게 안전
  • 로그가 많이 쌓일 수 있으니, 필요하면 로그 백업도 고려

📌 자동 인덱스 유지보수 스크립트 (Standard Edition용)

USE B;
GO

DECLARE @TableName NVARCHAR(128);
DECLARE @IndexName NVARCHAR(128);
DECLARE @Frag FLOAT;
DECLARE @SQL NVARCHAR(MAX);

DECLARE cur CURSOR FOR
SELECT
    t.name AS TableName,
    i.name AS IndexName,
    ips.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
JOIN sys.tables t ON ips.object_id = t.object_id
WHERE i.type_desc IN ('CLUSTERED', 'NONCLUSTERED')
  AND ips.page_count > 100; -- 작은 인덱스 제외

OPEN cur;
FETCH NEXT FROM cur INTO @TableName, @IndexName, @Frag;

WHILE @@FETCH_STATUS = 0
BEGIN
    IF @Frag BETWEEN 10 AND 30
        SET @SQL = 'ALTER INDEX [' + @IndexName + '] ON [' + @TableName + '] REORGANIZE;';
    ELSE IF @Frag > 30
        SET @SQL = 'ALTER INDEX [' + @IndexName + '] ON [' + @TableName + '] REBUILD;';
    ELSE
        SET @SQL = NULL;

    IF @SQL IS NOT NULL
    BEGIN
        PRINT '실행: ' + @SQL;
        EXEC (@SQL);
    END

    FETCH NEXT FROM cur INTO @TableName, @IndexName, @Frag;
END

CLOSE cur;
DEALLOCATE cur;
GO

 

 

 

터미널을 새로 하나 열어서

find /var/opt/gitlab/git-data/repositories -exec chown git:git {} \;

입력하고 컨테이너를 재시작한다.

 

출처: https://gitlab.com/gitlab-org/charts/gitlab/-/issues/5546#note_2017038672

원인은 세션별 동시에 읽어들일 수 있는 파일수가 정해져 있는데 초과할 경우 발생한다.

해결은 mac이나 linux면 그 숫자를 늘려주면 된다고 한다.

문제는 windows

범인은 swc였다.

babel을 활성화하면 해결된다.

느린 게 때론 괜찮을 때도 있다.

1. 현재버전 확인하기

docker image의 버전이 lastest라면 도움말에서 현재 사용중인 버전을 확인할 수 있다.
/help

 

2. 업그레이드 계획 세우기

https://gitlab-com.gitlab.io/support/toolbox/upgrade-path

에서  

현재버전부터 올리고 싶은 버전까지 선택

 

summary 와 18.3.1 사이의 … 을 누르면 단계별로 거쳐야하는 버전이 나온다
이렇게....

    각 버전들의 이미지를 미리미리 받아놓는게 편하다. 

3. 컨테이너 교체
    시놀로지 도커에서 기존 컨테이너를 지우고 2번에서 알려주는 다음버전으로 새 컨테이너를 생성한다.
    폴더 경로는 당연히 기존과 같아야 한다.

4. 컨테이너 생성후 시작한지 대충 3-4분 이후부터는 웹 로그인이 가능하다.

5. 관리자 영역 => 모니터링 => 백그라운드 마이그레이션 메뉴에서 Queued에 있는 것들이 완료로 다 가면 컨테이너 종료.
     /admin/background_migrations

6. 2번에서 그 다음버전 확인하고 3번부터 반복.
    2에서 제공하는 각 버전들의 이미지들을 미리 받아놨다면 완료된 이미지는 지우면서 하면 할만하다.

캡쳐는 15.0.5부터로 되어있지만 14부터 올리고 있었는데 15.0.5까지는 얼마 안 걸리길래 우습게 봤더니 15.4.6으로 올리고 있는데 5번이 오래걸리는 꼴이 버전 1개 올릴때마다 대략 30분 걸린다는 말이 허언이 아닌 것 같다. 나스라서 그런지 40분 이상 걸릴때도 있다.
이러다보니 하루 날잡아서 하기 보다는 점심시간이나 퇴근하기 직전에 하나씩 올리는 것이 낫겠다 싶다.

요약: 애초에 인코딩은 꼭 utf8로 설치하고 데이터베이스 생성시 collate는 "C.utf8"로 하자
백업복원으로 재생성할 수 없다면 일일히 수정해야 한다

ALTER TABLE [테이블명]
ALTER COLUMN [컬럼명]
SET DATA TYPE character varying(10) COLLATE "C.utf8";

테스트링크: https://onecompiler.com/postgresql/43pk5sws4

테스트코드

SHOW lc_collate;

SELECT * FROM pg_collation WHERE collname like 'ko%' or collname like '%utf%';

-- create
CREATE TABLE test_table (
  seq SERIAL PRIMARY KEY,
  cd VARCHAR(10) NOT NULL,
  nm VARCHAR(10)  NOT NULL,
  nm2 VARCHAR(10)  NOT NULL COLLATE "C.utf8"
);

-- insert
INSERT INTO test_table (cd, nm, nm2) VALUES ('BROWN', '브라운', '브라운');
INSERT INTO test_table (cd, nm, nm2) VALUES ('FUBAO', '푸바오', '푸바오');
INSERT INTO test_table (cd, nm, nm2) VALUES ('ETC', 'ETC', 'ETC');
INSERT INTO test_table (cd, nm, nm2) VALUES ('NO', '123', '123');

-- fetch 
SELECT a.* FROM test_table as a ORDER BY nm;
SELECT a.* FROM test_table as a ORDER BY nm2;

 

 

이하는 삽질로 얻은 지식 정리

1. 데이터베이스 스키마의 locale 확인

SHOW lc_collate

- 이것은 변경이 불가능하다. 데이터베이스 스키마를 새로 생성해야한다. 그리고 이것으로 기본 정렬된다.

- SELECT * FROM pg_database 에서는 모든 데이터베이스 스키마의 정보를 확인할 수 있고 UPDATE pg_database  SET  어쩌고 해서 수정할 수도 있으나 정렬 자체는 변하지 않았다.

 

2. 해당 스키마에서 사용할 수 있는 collate 확인

SELECT * FROM pg_collation 

한글 윈도우에서 기본 로케일로 설치한 것(이하 윈도우)은 utf8 관련은 전혀 없으나
docker로 설치한 것(이하 도커)은 collname기준 "C.utf8", "en_US.utf8" 두가지 있었다

=> 결론. 윈도우라도 인코딩 utf8로 설치해놔야 배포서버와의 환경차이로 인한 혼란을 줄일 수 있다.

 

3. 2의 테이블에 없는 것은 추가할 수 있다.

https://postgresql.kr/docs/13/sql-createcollation.html

ex) CREATE COLLATION [collation이름] (provider = icu, locale = 'ko_KR.utf8')

 

4. 데이터베이스의 collate를 바꾸지 않고 쿼리문에서 일시적으로 collate를 적용해서 정렬하는 방법

SELECT * FROM [테이블명] ORDER BY [컬럼명] COLLATE "ko-KR-x-icu"

collate 이름은 반드시 쌍따옴표로 해야 하며, 3의 테이블에서 collname컬럼을 사용해야 한다.
그런데 ko로 시작하는 것들로 정렬해보니 영어보다 한글이 먼저 나온다ㅠㅠ

 

'DB' 카테고리의 다른 글

InfluxDB2 backup  (0) 2024.02.06

1. 데이터타입: geography

2. 저장: 

INSERT INTO [테이블명] ([geography컬럼명]) values (geography::Point([위도], [경도], 4326));
-- 4326은 우리가 흔히 사용하는 좌표 CRS임

3. 문자열로 변환 조회
단, 이 경우 "POINT ([경도] [위도])" 형태로 반환되므로, 각각 decimal type의 컬럼으로 저장하는 편이 나았음 (다른 방법이 있을 수 있음)

SELECT convert(nvarchar(50), [geography컬럼명]) as [별칭] FROM [테이블명];
-- POINT ([경도] [위도])

4. 특정 위치를 기준으로 반경 5km 조회하기 - geography 타입을 사용한 이유

-- STDistance는 차이를 미터로 반환함

-- 1. 변수선언이 가능한 경우
DECLARE @Origin GEOGRAPHY
SET @Origin = GEOGRAPHY::Point([위도], [경도])
SELECT * FROM [테이블명] WHERE @Origin.STDistance([geography컬럼명]) <= (5 * 1000);

-- 2. DB에서 바로 조회할 경우.
SELECT * FROM [테이블명] WHERE (SELECT [geography컬럼명] FROM [테이블명] WHERE [관리키컬럼명]=[관리키]).STDistance([geography컬럼명]) <= (5 * 1000);

 

작년 초에 했었는데 1년 6개월만에 완벽하게 잊어버린 관계로 앞으로 이 사태를 막기 위해 기록으로 남겨둔다. cmd 로 발급하는 방법도 있지만 옵션이 뭐가 복잡하고 어쩌고 해서 알못도 이 방법은 쉽게 가능하니까

 

1. 아무 경로에나 폴더 생성

2. cmd 에서 npm init -y

3. npm i mkcert@1.5.1 (오늘자 기준으로 최근 버전인 3.2는 인증기관 인증서를 설치할때 에러가 발생하기 때문에 과거에 잘 됐던 버전으로 특정함)

4. common js 문법을 쓸거기 때문에 js 파일 하나 생성. main.js로 명명하겠음

5. 코드 작성

const mkcert = require('mkcert')
const fs = require('fs')

// 인증 기관 생성
mkcert
   .createCA({
      organization: '가상의 인증기관이름',
      countryCode: 'KR',
      state: 'SEOUL',
      locality: 'SEOUL',
      validityDays: 365, // 1년
   })
   .then((ca) => {
      fs.writeFileSync('./certs/ca.key', ca.key)
      fs.writeFileSync('./certs/ca.crt', ca.cert)

      // 그런 다음 TLS 인증서를 생성
      mkcert
         .createCert({
            domains: ['127.0.0.1', 'localhost'],
            validityDays: 365,
            caKey: ca.key,
            caCert: ca.cert,
         })
         .then((cert) => {
            fs.writeFileSync('./certs/cert.key', cert.key)
            fs.writeFileSync('./certs/cert.crt', cert.cert)
         })
   })

6. cmd에서 node ./main.js 실행

7. 인증기관 인증서 설치 진행

여기서부터 중요함. 인증기관 인증서 부터 설치를 해야함. 인증기관을 신뢰할 수 있어야 tls 인증서가 문제없다고 판단하여 설치할 수 있기 때문임. ca.crt 실행

 

8. 끝

 

기타정보

1. pem 파일은 crt 파일과 동일하기 때문에 확장자만 바꾸면 된다.

2. fullchain.pem 파일이 필요하다면 ca.crt, cert.crt 파일을 합치면 된다.

3. localhost를 https로 쓰다보면 http로 들어가고 싶어도 웹브라우저가 자동 리다이렉트를 해버려서 불편할 수 있는데 그때는 크롬 기준으로 chrome://net-internals/#hsts 로 접속하여 가장 하단인

여기에 localhost 를 넣어서 삭제하면 된다.

 

그럼 진짜 끝

'Server > 기타' 카테고리의 다른 글

리눅스 폴더별 압축  (0) 2022.06.22
쉘스크립트 - 파일을 폴더 생성 후 이동  (0) 2022.06.22

your system software has rejected this edit

이 오류 메시지가 뜨는 경우 번거로워도 불필요한 프로그램 설치없이 설정하려면 다음과 같은 방법으로 해야 한다.

1. 개발자 옵션을 활성화 한다.
설정 > 휴대전화 정보 > 소프트웨어 정보 > 빌드번호 항목을 연속하여 터치(클릭)하여 개발자 옵션을 활성화 한다.

2. usb 디버깅 모드를 활성화환다.
설정 > 개발자 옵션 > USB 디버깅을 허용으로 설정한다.

3. PC에 Google에서 제공하는 platform-tools를 다운로드 받아 압축을 해제한다. 한글 경로가 없도록 c:나 d: 등의 root에 압축을 해제하는 편이 편리한다.
다운로드: https://developer.android.com/studio/releases/platform-tools?hl=ko

4. 파워쉘이나 cmd를 실행하여 해당 경로로 이동하거나 해당 경로에서 오픈한다. (탐색기에서 shift + 우클릭하여 실행하거나 상단 경로에 cmd 입력 후 엔터 등)

5. 휴대폰을 PC와 연결한다. 충전케이블 말고 반드시 데이터케이블을 이용해야 한다.

6. cmd창에서

adb devices

 

7. 휴대폰 화면에 뜨는 USB 디버깅 확인 메시지에서 허용한다.

8. SetEdit에서 했던 설정을 여기서 입력한다.

adb shell settings put system csc_pref_camera_forced_shuttersound_key 0

 

9. 기본카메라에서 무음으로 사진이 잘 찍히는지 확인한다.

 

+ Recent posts