반응형

스피나커 브래드너

홍콩의 마이크로 브랜드 스피나커

최근에 슈퍼컴프레서 방식(이너베젤) 의 다이버 시계의 관심도가 높아지면서 선택한 스피나커의 브래드너라는 시계입니다.

더 고가 브랜드의 시계도 있었으나 자금 사정도 여의치 않고 느낌만 느끼고 싶은데다가 서플라이 루트라는 쇼핑몰에서 마침 세일도 하기에  선택하게 되었네요

전문지식은 거의 전무 하니 상품을 받고 느낀 점에 대해서만 리뷰하겠습니다.

겉 포장과 내부 박스

택배 박스에서 꺼낸 스피나커 포장 박스인데 내부 박스크기와 딱맞지 않고 좀 더 큰 종이 포장이였습니다. 무엇인가 구성품 중 빠져있는 느낌을 받았는데요 스피나커의 다른 상품을 사본적이 없어 원래 이런것인지 빠진건지 알수는 없었습니다.

종이박스에서 꺼낸 시계가 들이있는 박스는 스마트폰 박스랑 비슷한 느낌이였습니다. 심플한 느낌은 나쁘지 않았어요.

박스 개봉 샷

박스를 열어보면 시계와 우측 구석에 워런티 카드가 들어있는 심플한 구성이였습니다. 타이달 블루와 애틀랜틱 블루 색상이 있었는데 타이달 블루의 가죽밴드 색이 더 맘에 들어서 타이달 블루를 선택했는데 실물이 더 맘에 드네요.

 

브래드너 후면

브래드너 모델은 마이크로브랜드들의 저렴한 모델들의 동반자라고 하는 세이코 NH35 무브먼트를 사용하고 있으며

글라스는 무반사 코팅이 된 사파이어 글라스를 적용했다고 합니다. 

무브먼트는 저렴한 모델이니 당연히 일오차가 크겠지만 정확한 시간에 맞춰 살지 않기도 하고 스마트폰 시간보다 조금 더 일찍 움직이는 편이라 저에겐 중요도가 높지 않았습니다.

브래드너의 직경은 42mm

지샥 프로그맨처럼 완전 큰 시계를 좋아하지만 브래드너도 손목위에선 작지만은 않네요. 브래드너 사기 전까진 제임스 홀튼의 더티더즌 모델을 주로 착용했었는데 38mm 모델과 비교하니 많이 커보이긴했습니다. 시계에 관심있는 분들은 직경보다 러그 투 러그의 크기를 더 자세히 보시는거 같더라구요. 러그의 길이에 따라 직경대비 크기가 많이 좌우 되는거 같았습니다.

브래드너의 두께는 15mm

실착해보면 정말 두꺼워 보입니다. 직경 대비 두께가 두껍다고나 할까요? 엄청 두껍다라는 감탄사가 나올 정도였습니다. 하지만 지샥을 즐겨 착용하기도 하고 드레스 워치가 아니기에 전혀 문제되지 않았습니다. 두꺼운거 싫어하는 분들에게는 인기가 없을거 같아요

세이코 블랙몬스터

시계에 크게 관심없지만 예뻐서 10여년전에 샀던 세이코 블랙몬스터도 나름 두껍다고 생각했었는데 그 보다도 조금 더 두껍습니다.

두께 비교샷

실제 옆에 나란히 두고 비교해보면 블랙몬스터는 나토밴드의 두께도 포함되어 있음에도 브래드너가 두껍습니다. 

세이코 블랙몬스터와 스피나커 브래드너

베젤이 바깥에 있는 전통적인 다이버시계 형태인 세이코 블랙 몬스터와 비교샷 입니다.

브래드너의 이너베젤은 2시방향에 있는 용두를 돌려주면 되는데 이게 너무 가볍게 돌아가는게 아쉬웠습니다. 어딘가에 살짝 닿아도 돌아간다고 할까요 그정도로 가볍게 돌아갑니다. 

발광 비교

다이버 시계들의 필수 조건중 하나인 야광

발광은 블랙몬스터가 오래된 시계임에도 더 밝은거 같지만 브래드너의 발광도 나쁘지는 않았습니다.

다만 낮동안 착용후 밤에 봤을때 브래드너의 발광은 조금 아쉽긴 했습니다만 야광의 색이 블몬보다 더 맘에 드네요.

 

시계의 디테일이나 마감같은건 두눈 부릅뜨고 보는 타입은 아니기도 하고 비교할 만큼 좋은 시계를 가지고 있지 않아 해당부분에 대한건 잘 모르겠습니다.

처음 사본 해외의 마이크로 브랜드 시계인데 잘 샀다는 생각은 듭니다. 

이너베젤의 다이버 시계를 가볍게 경험해보고 싶은 분이라면 나쁘지 않은 선택이 될거 같아요.

 

 

 

 

728x90
반응형
반응형
  • 일반 쿼리와 같이 특정 조건에 맞는 데이터를 찾고자할때 where를 사용한다. 
  • 예를 들어 현재 시간과 같거나 이전에 등록된 상품을 조회한다고 한다면 아래와 같이 쓰게 된다.
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .get();
  • 조회한 데이터를 sort시킨다고 하면 이 또한 일반 쿼리에서 사용하는 orderBy를 사용하면 된다.
  • 예를 들어 등록된 시간별로 sort시켜보자.
// 내림 차순
var snapshot = await FirebaseFirestore.instance.collection('product')
        .orderBy('regTime', descending: true)
        .get();
        
// 오름 차순
var snapshot = await FirebaseFirestore.instance.collection('product')
        .orderBy('regTime')
        .get();
  • 만약 sort 조건으로 두가지 이상을 사용한다고 하면 orderBy 뒤에 추가로 .orderBy(${field})를 추가해주면 된다.
  • 단 firebase는 단일 필드에 대한 인덱스는 자동으로 제공하지만 두가지 이상에 대해서는 직접 색인(index)를 추가해주어야 한다.
  • 그럼 where와 orderBy를 같이 사용하려고 한다면 어떻게 될까? 
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .orderBy('productName', descending: true).orderBy('price')
        .limit(5)
        .get();
  • 위의 코드와 같이 where조건과 orderBy를 진행했으나 원하는 결과가 아닌 오류가 발생하였다.
Failed assertion: line 487 pos 13: 'conditionField == orders[0][0]': 
The initial orderBy() field "[[FieldPath([productName]), true]][0][0]" 
has to be the same as the where() field parameter "FieldPath([regTime])" 
when an inequality operator is invoked.
  • where절에 사용한 parameter와 뒤에 이어지는 orderBy가 같아야 한다는 의미 메시지를 던져주게 된다.
  • 결과적으로 where와 orderBy를 같이 사용할때는 where절에 사용한 필드에 대한 orderBy가 바로 뒤에 따라오고 그 다음에 원하는 orderBy를 사용해야만 한다.
var snapshot = await FirebaseFirestore.instance.collection('product')
        .where('regTime', isLessThanOrEqualTo: DateTime.now().toString())
        .orderBy('regTime', descending: true)
        .orderBy('productName', descending: true).orderBy('price')
        .limit(5)
        .get();
  • 그리고 저렇게 된다면  where절에 사용한 필드를 orderBy에 사용했기때문에 firebase의 색인에 위의 코드에서 orderBy에 사용한 세가지 필드의 sort조건에 맞는 색인을 만들어 두어야 한다.
728x90
반응형
반응형

혼자 프로젝트를 진행하면 어떤 파일을 push해도 상관없지만 팀원들과 일을할때는 올리지 말아야하는 파일들이 존재한다.

IDE관련 설정파일이던가 build와 관련된 파일들 같이 개인과 관련된 파일들인데 일을 하다보면 종종 파일이 공유되는 문제가 발생한다.

분명 ignore에 정의 되어 있는데 이런 문제가 발생하는 이유는 무엇일까?

프로젝트를 생성하고 git branch에 처음 push 하기 전에 gitignore를 잘 정의했다면 해당 문제는 발생하지 않는다. (100%는 아님)

gitignore를 다 정의하지 않고 내 로컬에서 생성된 모든 파일을 branch에 올리게 되면 대상 파일들은 모두 git이 트래킹하기 시작한다.

그래서 그 뒤에 gitignore를 정의하면 해당 내용이 제대로 반영되지 않는 문제가 발생한다.

본인이 신중해서 git에 push 하기전에 올릴 파일들을 선별하고 대상 파일만 add, commit, push를 한다면 gitignore가 적용되든 안되든 상관없겠지만 일반적으로는 add . 으로 모든 파일을  staged 에 올려서 commit, push 하는경우가 더 많다.

그럼 이미 트래킹 되고 있는 상태에서 gitignore를 반영할 수 있게 해보자.

먼저 트래킹에서 제거하고자 하는 대상을 .gitignore 에 정의한다.

git rm -r --cached .
git add .
git commit -m "Fix untracked files"

git rm -r --cached . 명령어로 캐시된 내용을 삭제 하고  git status 명령어를 실행하면 .gitignore에 추가한 항목들에 deleted 라고  상태가 정의된 것을 볼 수 있다.

git add -> commit -> push 하면 원격지에 트래킹 제거 대상들이 정리가 되고 추후 트래킹이 되지 않는걸 확인 할 수 있다.

728x90
반응형
반응형
  • 테스트 목적으로 빌드한 apk를 디바이스에 설치했을 때 발생한 이슈
  • flutter로 프로젝트 생성 후 android의 AndroidManifest.xml을 확인해보면 아래 코드가 기본값으로 설정 되어 있음.
<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
  • 앱을 만들기 위해 외부 라이브러리를 사용하다보면 AndroidManifest.xml의  intent-filter에 정보를 넣게 되는 경우가 발생하게 됨.
<intent-filter>
   <action android:name="android.intent.action.MAIN"/>
   <category android:name="android.intent.category.LAUNCHER"/>
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
   <data
       android:host="arandom.page.link"
       android:scheme="https" />
</intent-filter>
  • 위 코드 상태로 build를 하여 apk를 생성 후 디바이스에 설치해보면 설치 완료 후 열기버튼이 비활성화가 되고 앱 아이콘도 생성되지 않는 문제가 발생함
  • 애플리케이션 리스트에는 설치되어 있다고 뜨지만 실행할 방법이 없음
  • 해결방법
<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
   <data
       android:host="arandom.page.link"
       android:scheme="https" />
</intent-filter>
  • 프로젝트 생성시 기본값으로 생성되는 코드는 건드리지 않고  <intent-filter>블록을 추가 하면 해결됨
  • 네이티브에 대한 이해도가 낮다보니 문제는 해결했으나 이유를 정확히 모르고 있기때문에 추가적으로 공부가 필요하다는 결론......
728x90
반응형
반응형

터미널을 통해 SSH 접속 시 "Host key verification failed" 오류가 발생하는 이유는 클라이언트가 접속하려는 서버의 호스트 키를 검증할 수 없기 때문입니다. 

터미널에 아래와 같은 메시지가 보여지게 됩니다.

ssh -i rsa_4096 id@127.0.0.4

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:
Please contact your system administrator.
Add correct host key in /Users/hajinho/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/hajinho/.ssh/known_hosts:26
Host key for 127.0.0.4 has changed and you have requested strict checking.
Host key verification failed.

 

 

이 오류는 여러 가지 이유로 발생할 수 있습니다. 다음은 그 이유와 해결 방법입니다.

1. 서버의 호스트 키가 변경된 경우

서버의 호스트 키가 변경되면 클라이언트의 ~/.ssh/known_hosts 파일에 저장된 키와 일치하지 않기 때문에 오류가 발생합니다. 서버가 재설치되거나 SSH 설정이 변경되면 호스트 키가 변경될 수 있습니다.

해결 방법: 기존의 호스트 키를 삭제하고 새로운 호스트 키를 추가합니다.

ssh-keygen -R [hostname or IP address] 
ssh [hostname or IP address]

2. 서버의 IP 주소가 변경된 경우

서버의 IP 주소가 변경되었지만 호스트 이름은 변경되지 않은 경우에도 호스트 키 검증 오류가 발생할 수 있습니다. 클라이언트는 호스트 이름과 연결된 이전 IP 주소의 호스트 키를 기억하고 있기 때문입니다.

해결 방법: ~/.ssh/known_hosts 파일에서 해당 서버의 이전 엔트리를 삭제합니다.

ssh-keygen -R [hostname] 
ssh [hostname]

3. Man-in-the-Middle 공격 가능성

호스트 키 검증은 Man-in-the-Middle (MITM) 공격을 방지하기 위해 사용됩니다. 호스트 키가 예상치 않게 변경되었을 때 이 오류가 발생하면 MITM 공격의 가능성을 의심해봐야 합니다.

해결 방법: 서버 관리자에게 문의하여 호스트 키가 변경된 것이 정상인지 확인합니다. 변경이 정상적이라면 새로운 호스트 키를 수동으로 추가합니다.

4. 클라이언트의 ~/.ssh/known_hosts 파일이 손상된 경우

클라이언트의 ~/.ssh/known_hosts 파일이 손상되었거나 잘못된 형식으로 인해 호스트 키를 검증할 수 없는 경우에도 이 오류가 발생할 수 있습니다.

해결 방법: ~/.ssh/known_hosts 파일을 확인하고 문제가 있는 부분을 수정하거나 필요한 경우 파일을 삭제한 후 다시 서버에 접속하여 호스트 키를 추가합니다.

rm ~/.ssh/known_hosts 
ssh [hostname or IP address]

 

 

위의 방법 중 하나를 통해 "Host key verification failed" 오류를 해결할 수 있습니다.

728x90
반응형
반응형
  • 삼항연산자
    • 조건 ?  A : B
    • 조건에 만족하면 A를 리턴하고 만족하지 못하면 B를 리턴
  • 이중물음표(??)
    • A ?? B
    • A의 값이 null 이면 B를 리턴하고 null이 아니면 A를 리턴
  •  변수 A == null ? B : A 라는 삼항연산자를 사용하여 null 여부를 체크하는 경우라면   A ?? B 라고 이중 물음표 표현식을 사용하면 간소화 할 수 있음
  • null 체크가 아니라면 삼항 연산자를 사용하면 됨
728x90
반응형
반응형
  • 데이터 베이스에서 조회한 데이터를 opencsv를 사용하여 csv파일로 저장하는 코드
val writer: FileWriter = FileWriter("output.csv")
val strategy: CustomMappingStrategy<ReportDTO> = CustomMappingStrategy<ReportDTO>()
strategy.type = ReportDTO::class.java

val beanToCsv: StatefulBeanToCsv<ReportDTO> =
    StatefulBeanToCsvBuilder<ReportTravelDTO>(writer)
        .withMappingStrategy(strategy)
        .build()

beanToCsv.write(travelDataList)
writer.close()

해당 코드 결과는 이렇게 나온다.

"seq","gender","region2"
"296","1",""

하지만 원하는 형태는 데이터가 있는 경우에는 큰따옴표를 제거하고 null이거나 데이터 내부에 콤마(,)가 들어있는 경우에 한해서만 큰따옴표로 묶는것이기에 다른 방법이 필요했음.

opencsv에서 제공하는건 withApplyQuotesToAll(false) 옵션으로 설정했을때 내용에 콤마가 있는경우는 큰따옴표로 묶어주지만 공백일 경우에는 묶어주지 않는다.

원하는 조건을 만족시키려면 결국 CSVWriter를 상속받아서 stringContainsSpecialCharacters를  overriding 해야 함.

import com.opencsv.CSVWriter
import java.io.Writer

class CustomCsvWriter(writer: Writer?) : CSVWriter(writer) {

    override fun stringContainsSpecialCharacters(line: String): Boolean {
        var result: Boolean = false

        if (line.isEmpty()) result = true
        if (line.contains(',')) result = true
        return result
    }
}

내가 원하는 조건에 true값을 주고 실제 csv를 만드는 코드에서 withApplyQuotesToAll(false)를 설정해야 커스텀 한 결과가 반영된다.

 

val writer: FileWriter = FileWriter("파일.csv", Charset.forName("utf-8"))  
val strategy: CustomMappingStrategy<ReportDTO> = 
	CustomMappingStrategy<ReportDTO>()
    
strategy.type = ReportDTO::class.java

val beanToCsv: StatefulBeanToCsv<ReportDTO> =
    StatefulBeanToCsvBuilder<ReportTravelDTO>(CustomCsvWriter(writer))
        .withMappingStrategy(strategy)
        .withSeparator(CSVWriter.DEFAULT_SEPARATOR)
        .withApplyQuotesToAll(false)
        .build()

beanToCsv.write(DataList)
writer.close()

커스텀한 CsvWriter를 적용한 코드의 결과는

seq,gender,region2
296,1,""

원하는 구조로 바뀐걸 확인 할 수 있다.

 

728x90
반응형
반응형
  • GetX 를 사용하여 페이지 전환
    • Get.toNamed('호출 페이지')를 사용하면 호출 페이지로 화면이 전환되어 짐
    • ex) Get.toNamed('/event')
    • Navigator.of(context).pushNamed('호출 페이지)')를 사용해도 동일한 결과를 얻을 수 있음
  • toNamed사용시 arguments 전달
    • Navigator를 사용하는 경우 다소 복잡한 절차가 필요함
    • 일단 빠르게 만들기 위해 GetX의 기능을 사용하기로 함
    • Get.toNamed('/event', arguments: {'choice': 'codeA'})와 같은 형태로 호출하면 됨
  • /event페이지에서 arguments사용
    • Navigator를 사용할 경우와 다르게 arguments를 받기 위한 생성자나 변수가 필요하지 않음
    • 해당 페이지내에서 arguments를 사용하고자 하는 위치에서 Get.arguments['choice'] 사용하면 'codeA'를 리턴해줌

샘플 소스

//event_main page
eventEnter(int index) async {
    final result = await Get.toNamed('event_enter', arguments: {'choiceCode' : index == 0 ? 'a' : 'b' });
    if (result == true) {
      setState(() {
        .....;
      });
    }
  }
  
  
  // event_enter page
  class EventEnterView extends StatelessWidget {
  	const EventEnterView({Key? key}) : super(key: key);

  	@override
  	Widget build(BuildContext context) {
    	return Container(
        	.
            .
            .
            child: ElevatedButton(
              onPressed: () async {
                Map<String, dynamic> data = {
                  'choice': Get.arguments['choiceCode'],
					.
                    .
                    .
                };
              }
            .
            .
            .
        );
    }

 

 

 

728x90
반응형

+ Recent posts